ORM 기술을 적용하다보면 엔티티를 쉽게 마주치게 된다.
영속성 관리를 위한 객체이기 때문!
스프링을 MyBatis가 아니라, JPA로 처음 접한 나는 당연히 여태까지 @Entity 어노테이션을 활용해서 잘 쓰고 있었다.
그런데 문득, 새로운 프로젝트를 시작하면서 엔티티를 새로 여는데 엔티티에 쓰이는 어노테이션이 어떤게 있고 왜 쓰지?
이게 궁금해져서 정보를 수집한 뒤, 글을 남겨본다...!
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 100, nullable = false)
private String email;
@Column(length = 100, nullable = false)
private String pw;
@Column(length = 100, nullable = false)
private String name;
}
지금 말하려는 주제의 핵심은 아니지만, 혹시 궁금한 사람이 있을까봐 내부에 쓰인 어노테이션에 대해 말하자면 다음과 같다.
자, 이제 핵심을 말하자면 @Entity외에 쓰이는 어노테이션을 봐야한다.
당연하게도? 대부분 Lombok 어노테이션을 활용하는데, 가장 자주 쓰이는 어노테이션들은 다음과 같다.
테이블 어노테이션은 뜻 그대로 테이블과 연결시켜주기 위해 쓴다.
예를 들어서 실제 디비의 테이블 네임은 abc 인데, 엔티티의 이름은 abcd 라고 하면 자동으로 매칭될 수 없다.
그래서 보통은 엔티티와 테이블네임을 일치시켜주는 게 아주 일반적인 것으로 알고 있는데, 경우에 따라 그럴 수 없기도 하다.
바로 그럴때에 @Table 어노테이션을 사용해서 테이블과 엔티티를 매칭시켜준다.
@Table( name = "abc" )
@Entity
public class abcd {
...중략
위와 같이 사용하면 abcd 엔티티를 abc 테이블에 매칭시켜준 것이다.
눈치 챘을 지 모르지만, 지금 쉬운 순서대로 정렬해서 설명하고 있다.
즉, 게터도 너무 당연하다.
엔티티의 필드를 조회해야할 경우는 반드시 있기 때문에 @Getter는 당연히 사용한다.
@Getter
@Table( name = "abc" )
@Entity
public class abcd {
...중략
게터의 개념까지 모르는 사람이 있을 거라고 생각하진 않기에 게터와 세터에 대한 얘기는 넘어가도록 하겠다!
이제 여기서부터가 흥미진진해진다.
일단, 이 어노테이션이 어떤 역할을 수행하는지 알아야한다.
@NoArgsConstructor는 해당 클래스에 기본 생성자를 만들어준다.
java의 ORM 기술인 JPA는 기본 스펙상 기본 생성자를 요구한다. (없으면 에러남)
마치 Querydsl 생성할때 super 클래스로 도메인 클래스 넘겨줘야하는 것과 같다.
즉, 그냥 그 기술을 만들어놓은 사람들이 설계상의 이유로 그렇게 해놨고 그걸 지켜야 한다.
깃허브의 소스들을 둘러보면 @NoArgsConstructor 어노테이션이 붙어있지 않은 경우도 많기 때문이다.
그 이유는 바로, @Entity 어노테이션 덕분이다.
사실 엔티티 어노테이션은 내부적으로 기본 생성자를 만들어준다.
즉, 자동인 셈이다.
자동이면 안써도 되는거고 실제로 안쓴 사람들도 많은데 그럼 쓴 사람들은 뭐지?
쓴 사람들은 대게 @NoArgsConstructor 에서 그치지 않고 그 외에 부가적인 것들이 들어있다.
먼저 예를 들어서 이런 소스코드를 주로 사용한다.
@NoArgsConstructor( access = AccessLevel.PROTECTED)
@Getter
@Table( name = "abc" )
@Entity
public class abcd {
...중략
(어노테이션을 붙이는 순서는 그저 설명 순서에 따른 것이니 신경 쓰지 않아도 된다.)
위처럼 @NoArgsConstructor 를 쓰는 사람들은 괄호 안에 접근 조건을 걸어둔 경우가 많다.
ORM 기술이라는 것은 기본적으로 영속성 이라는 개념이 있다.
간단히 말해, 데이터베이스의 실제 데이터와 그 엔티티가 일관된 상태를 유지하고 있어야한다는 것이다.
그런 이유로 엔티티에는 @Setter 어노테이션을 붙이지 않는다.
각 필드들에 대해 일관성을 유지해야하는데..
언제, 어느 코드에서, 누가, set() 를 여는 순간 그게 다 깨지기 때문이다.
그럼에도 어쨌든 값을 넣어주긴 해야되니까 생성자를 쓰는 것이다.
실제로 패턴상 안전한 방법이기 때문!
그래서 @NoArgsConstructor 를 활용해서 기본생성자를 추가하되, 접근제한을 걸어서 안전성도 높이는 것이다.
위에서 JPA는 기본 생성자를 요구한다고 했었다.
그런 JPA가 받아들일 수 있는 최대 수준의 생성자가 Protected 이기 때문이다.
즉, 할 수 있는 선에서 최대한의 접근 제한을 건 것이라고 생각하면 된다.
이제 마지막이다...!
@AllArgsConstructor
@NoArgsConstructor( access = AccessLevel.PROTECTED)
@Getter
@Table( name = "abc" )
@Entity
public class abcd {
...중략
@AllArgsConstructor 는 전체 필드에 대한 생성자이다.
즉, name, age, email 이라는 필드가 있으면 그 3개에 대한 전체 생성자라는 뜻이다.
@AllArgsConstructor를 왜 쓰는지 알려면 다시 3번째 스텝의 @No.. 로 돌아가야된다.
위에서 @NoArgsConstructor( access = AccessLevel.PROTECTED) 이런식으로 사용한다고 했었다.
그런데 문제가 하나 있다.
말했다시피 ORM에서 중요한건 세터를 쓰지 않는다는 사실이다.
그리고 new() 방식으로 객체를 생성하는 것 또한 추천하지 않는다고한다.
당연히 일관성이 깨질 위험이 있기 때문이다.
그렇기 때문에 주로 빌더 패턴을 사용하게 된다.
빌더 패턴은 @Builder 어노테이션으로 사용하는데 바로 이 지점에서 문제가 생긴다.
빌더 패턴은 기본적으로 빌더 어노테이션이 적용된 전체 필드에 대한 값을 요구한다.
그래서 생성자가 반드시 필요하다.
분명 NoArgs... 를 사용하면 기본생성자가 만들어진다고 했었다.
그럼에도 불구하고 왜 문제가 생기는지 보면 접근 제한 때문이다.
빌더는 해당 클래스에 생성자가 없다면, 만들어주고 | 생성자가 있다면, 그걸 사용한다.
그럼 기존의 클래스는 @NoArgs..가 붙었으니까 생성자가 있다고 판단한다.
때문에 빌더는 생성자를 만들지 않고 이미 있는 생성자에 접근을 시도한다.
그런데 이게 Protected로 막혀있어서 접근을 거절당하고 결국 에러를 띄우게 되는 것이다.
결국 ORM의 중요한 덕목인 일관성을 지키며 안전성을 높이는 것.
그걸 해내기 위해선 빌더 패턴과 생성자를 적극적으로 활용해야하는데, 위에서 언급한 이슈 때문에 그게 안되는 상황이다.
근데 잘 생각해보면 공존할 수 있는 방안이 아주 쉬운게 하나 있다.
결국 빌더 패턴에서 요구하는 생성자는 전체 필드에 대한 생성자이다.
그러면 그냥 @AllArgsConstructor 를 주면 그만인 것이다....!
이건 또 @Entity 를 까보면 알 수 있다.
아까 위에서 @Entity는 생성자를 추가하는 코드를 쓰지 않아도 자동으로 기본생성자는 만들어준다고 했었다.
그런데 특징이 하나 있다.
만약, 이미 생성자가 있을 경우에는 만들지 않는다.
그렇기 때문에 @AllArgsConstructor를 적용하고 나면 기본생성자가 만들어지지 않는 문제가 생겨버리는 것이다.
그래서 바로바로바로 이런식으로 나는 코드를 짜고 있다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class User {
...중략
@All 생성자를 넣어줌으로써 빌더 패턴을 챙길 수 있고, @All 때문에 자동생성 되지 않은 기본 생성자를 @No.. 어노테이션으로 직접 추가해준다.
이렇게 하면 나의 이론상 가장 완벽한,,? 엔티티가 된다..^^
추가로 @AllArgsConstructor를 쓰지 않고 @NoArgs..에 Access 제한 걸어준 뒤에, 빌더 패턴을 적용할 수 있는 방법이 또 있다.
각 메서드 마다 @Builder 어노테이션을 붙여주면 된다.
빌더가 클래스에 붙어있어서 그에 대한 생성자에 접근하느라 생긴 문제기 때문에 메서드에 하나씩 걸어주면 문제가 되지 않는다.
다만 이렇게 되면 아무래도 귀찮으니까..
@Entity에 의해 자동으로 NoArgsConstructor가 생성되는게 맞나요 ? Java class 자체적으로 아무런 constructor가 명시되지 않으면 인자가 없는 constructor가 생성되는게 아닐까요 ??