티스토리 뷰
POJO 란 Plain Old Java Object 의 줄임말로 특별한 제약이 없는 객체를 의미한다.
특별한 제약이란 무엇일까? 객체 설계 관점에서 필요에 의한게 아니라 다른 외부 기술의 사용 때문에 객체에 제약이 생기는 경우이다.
예를들어 서블릿을 생각해보자. 서블릿을 이용해서 http 요청에 매핑하는 것과 스프링의 컨트롤러를 이용하는 코드를 비교해자.
// HttpServletRequest 를 상속받는다
public class HttpRequestMapping extends HttpServletRequest { }
// 애노테이션으로 요청을 받음을 표현한다
@Controller
public class HttpRequestMapping { }
서블릿 같은 경우 HttpServletRequest 를 필수로 상속 받아야하고, 스프링 컨트롤러는 애노테이션만 붙이게 된다. 이 둘은 어떤 차이가 있을까?
일단 자바에서 상속은 단일 상속만 지원된다. HTTP 요청을 처리하는 객체가 무언가의 이유로 상속을 활용해야하는 상황이 됐을때 서블릿 객체는 이미 기술적 이유로 상속을 받고있어 추가 상속을 받을 수 없다.
다음은 기술에 대한 의존이다. 두 클래스의 인스턴스를 생성자를 이용해 직접 생성해보자. 서블릿 객체의 인스턴스를 손쉽게 만들 수 있는가? 또한 상속을 통해 문제해결을 하고 있기 때문에 불필요한 메서드들의 공개를 막을길이 없다. 손쉽게 객체를 만들어 단위테스트를 작성한다고 가정했을때 스프링 컨트롤러가 압도적으로 쉽다. 물론 애노테이션이 붙었으니 스프링 컨트롤러도 완전한 POJO는 아니지 않냐고 의문을 제기하는 사례도 있다. 이는 좀 더 뒤에서 다뤄보도록 하겠다.
위 관점에서 JPA 엔티티는 POJO 인지 아닌지 항상 얘기가 많다. 이번 포스팅에서는 POJO 인지 아닌지를 알아보고, 실용적 관점에서 어떻게 바라볼 수 있는지 정리해보려 한다.
# JPA 애노테이션
가장 쉽게 이의를 제기할 수 있는 부분으로는 JPA 애노테이션이 객체에 침투한다는 것이다.
@Entity
public class Person {
@Id
private Long id;
private String name;
}
아주 간단한 객체인데 이를 JPA 엔티티로 정의하면 최대한 관례를 이용한다고 해도 위 2개의 애노테이션은 붙여줘야 한다. 명시적인 설정을 좋아하면 @Table, @Column 등의 애노테이션이 추가로 붙는다. 더욱이 실제 JPA 엔티티는 저정도로 간단하지 않으므로 더 많은 JPA 의존이 발생한다.
이는 JPA 엔티티는 POJO 가 아니라고 주장하는 의견 중 가장 쉽게 볼 수 있는 의견이며, JPA 가 classpath에서 사라지면 Person 클래스는 컴파일이 불가능해진다.
이 의견은 분명히 맞는말이나 POJO 에 대한 논의가 이루어질때 어느정도 수준의 순수함을 보장해야하느냐의 결정에 따라 달라질 수 있는 부분이다. 애노테이션은 상속처럼 외부 기술에 대한 종속을 강하게 결합하지 않으며, 컴파일만 가능하다면 원할때 외부 기술 없이 객체를 사용할 수 있기 때문이다. 위 Person 클래스의 경우 생성자나 메서드만 정상적으로 제공되고 있다면 JPA 애노테이션이 붙어있어도 인스턴스를 만들고, 사용하는데 아무런 제약이 없다. 다만 엄격한 관점에서는 애노테이션 참조도 침투로 보기 때문에 POJO 가 아니라는 주장도 합당하다.
## xml 매핑
// persistence.xml
<entity class="com.yong.jpa.jpa.person.Person" access="FIELD">
<table name="person"/>
<attributes>
<id name="id">
<column name="person_id"/>
<generated-value strategy="IDENTITY"/>
</id>
<basic name="name">
<column name="username"/>
</basic>
</attributes>
</entity>
// 설정 yml
spring:
jpa:
mapping-resources: META-INF/persistence.xml
하지만 JPA 가 애노테이션 설정이 아닌 외부에서 설정할 수 있다면 어떨까? 실제로 JPA 는 해당 기능을 제공하고 있으며 xml 로 매핑을 설정하면 JPA 관련 모든 애노테이션을 제거할 수 있다. JPA 는 xml 설정 방식과 애노테이션 방식을 모두 제공하고 있는데, 보통은 더 편리한 애노테이션 방식을 사용하는 것 뿐이다. 애노테이션 침투가 불편하다면 xml 방식을 사용하면 된다. 편의에 의해서 선택에 의해 애노테이션 방식을 사용하고 있으면서 JPA 엔티티는 POJO 가 아니라고 얘기할 수 있을까? 그럼 POJO 로 사용하기 위해 xml 매핑 방식을 사용할 것인가? 고민해볼만한 내용이다.
# id 할당
만일 애노테이션 기반이 아닌 xml 방식을 사용한다고 하면 그때는 JPA 엔티티를 완전하게 POJO 라고 부를 수 있을까? xml 방식을 이용한다고 가정하면 classpath에 JPA 가 사라져도 컴파일이 되는건 확실한 것이다. 그러면 이제 POJO 일까?
엔티티의 고유성은 내부 상태들을 기반으로 판단하기보다는 고유한 id로 판단한다. 두 객체의 상태가 달라도 id가 같으면 같은 엔티티로 판단한다는 것이다. 때문에 id를 할당하는 로직은 상당히 중요한데, 편리한 방식으로는 데이터베이스의 id 할당을 그대로 사용하는 것이다. 이 방식을 많이 사용하고 있기 때문에 이를 기반으로 설명하겠다.
public class Person {
private Long id;
private String name;
public Person() {}
public Person(Long id, String name) {}
}
@Test
void 엔티티를_저장하면_id가_할당된다() {
Person person = new Person(null, "name");
Person savedPerson = personRepository.save(person);
assertThat(savedPerson.getId()).isNotNull();
}
생성자를 이용해 person 을 초기화하고 저장했다. 저장한 다음엔 id가 데이터베이스를 이용해 할당되기 때문에 해당 테스트는 통과한다. id를 직접 할당할 일은 없으니 null을 매번 전달하는 것보단 해당 생성자를 없애는게 나을 수 있다.
public class Person {
private Long id;
private String name;
public Person() {}
public Person(String name) {}
}
@Test
void 엔티티를_저장하면_id가_할당된다() {
Person person = new Person("name");
Person savedPerson = personRepository.save(person);
assertThat(savedPerson.getId()).isNotNull();
}
불필요한 null 전달이 사라지고 더 깔끔한 코드가 됐다.
엔티티를 이용하는 다른 객체 IdChecker 가 있다고 가정해보자. 쉬운 예제를 작성하다보니 다소 억지스러울 수 있는 예제지만 현실에서 id를 꺼내서 사용하는 코드는 어렵지 않게 찾아볼 수 있다.
public class IdChecker {
public void check(Person person) {
if(person.getId() == null) {
throw new EntityIdCheckException();
}
}
}
@Test
void 정상적으로_초기화된_엔티티는_예외가_발생하지_않는다() {
IdChecker checker = new IdChecker();
Person person = new Person("name"); // id를 넣을 수 없다!
checker.check(person); // id가 없으니 예외가 발생하고, 테스트는 성공할 수 없다
}
Person 엔티티가 xml 을 이용해 매핑하도록 했다면, JPA 가 없는 상태에서도 정상적으로 컴파일이 된다. 하지만 JPA 가 id를 매핑한다는 가정하에 객체가 디자인됐다면 JPA 가 없는 상태에서는 (리플렉션을 쓰지 않는 한)완전한 상태로 초기화할 수 없다. 이는 바이너리 수준에서 기술 의존은 없지만 논리적으로 기술에 의존하게 되는 셈이고, 이런 디자인은 POJO 로 보기 어렵다. 객체 디자인에서도 JPA 가 초기화한다는 가정과 JPA 가 초기화하는 방식에 의존해선 안된다.
public class Person {
private Long id;
private String name;
public Person() {}
public Person(String name) {}
public Person(Long id, String name) {}
}
@Test
void 정상적으로_초기화된_엔티티는_예외가_발생하지_않는다() {
IdChecker checker = new IdChecker();
Person person = new Person(1L, "name");
checker.check(person); // id가 할당됐으니 테스트는 성공한다
}
정상적으로 완전한 인스턴스를 생성할 수 있는 방식을 제공해야 JPA 가 없는 상태에서도 단위 테스트를 통과할 수 있다.
# 완전한 POJO 로 가는 길
매핑 정보를 xml 로 설정하고, JPA 없이도 id 를 할당할 수 있다면 POJO 일까? JPA 스펙에는 엔티티 클래스가 가져야하는 규약이 있다. 이 규약들을 모두 숙지하고 엔티티를 만드는 경우도 있겠지만, 보통은 규약을 잘 모르는 상태에서 일반적인 클래스를 정의해도 문제없이 동작한다. 다만 한가지 규약은 많은 이들이 알고 있을텐데 proteced 혹은 public 의 기본생성자가 존재해야한다는 것이다. 이 규약 때문에 롬복을 이용하는 경우 이 애노테이션이 계속 붙게 될 것이다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {
private Long id;
private String name;
}
객체 디자인 관점에서 추가된 생성자가 아니라 기술에 의해 필요한 생성자이기 때문에 public 으로 공개하기는 싫고, 그보다 아래 공개 수준인 protected 로 선언하는 AccessLevel.PROTECTED 를 붙이는게 최선이다. 이는 현재의 JPA 스펙에선 필수 규약이기 때문에 객체 디자인에 기술이 침투하는 모습이 된다.
# 마무리
이 글에서 언급한 주제들과 예로 사용한 JPA 엔티티는 최소한의 설정을 갖고있는 엔티티이다. 보통의 실무에서는 이보다 더 복잡한 엔티티를 사용하게 되는데, 그런상황에서는 더 많은 주제로 얘기해볼 수 있을 것이다.
결론으로 얘기해서 JPA 엔티티는 완전한 수준에서 POJO 라고 보기 어렵다. 하지만 JPA 가 세상에 나오기 전엔 이보다 훨씬 기술의 침투가 만연했고, 그걸 최대한 극복해서 거의 POJO 처럼 사용할 수 있게한 공로는 인정해야한다. 설계는 항상 트레이드오프이고, 엄밀한 수준의 POJO 로 활용하려면 큰 노력을 요구하기 때문이다.
기회가 된다면 JPA 를 사용하면서 정말 POJO 를 사용하려면 어떻게 하면 좋을지 고민해보고 시도해보는 것도 좋을 것이다. 그러면 POJO 로 가기 위해 어느정도의 비용을 치뤄야하는지, 또한 타협을 한다면 실용적 관점에서 어디에서 타협을 할지도 고민해볼 기회가 될 것이라 생각한다.
'Java > jpa' 카테고리의 다른 글
| Hibernate 6.6.x merge 동작방식 변경 (2) | 2025.02.15 |
|---|---|
| JPA(Hibernate) 사용시 enum 필드에서 ArrayIndexOutOfBoundsException 가 발생한다면 (0) | 2024.12.21 |
| spring boot 2.x hibernate 5.x 에서 매핑키만 있고 데이터는 없는 경우 전체 엔티티 조회 실패 (0) | 2024.04.30 |
| Entity Merge 시에는 동일한 객체를 반환하지 않는 이유 (0) | 2023.05.07 |
| @DynamicUpdate 는 언제 써야할까 (0) | 2023.01.10 |
- Total
- Today
- Yesterday
- mariadb
- DesignPattern
- TEST
- spring cloud
- 정규표현식
- java
- backend개발환경
- OOP
- frontend개발환경
- programming
- java8
- MySQL
- Spring
- generics
- Git
- db
- javascript
- Kotlin
- go-core
- http
- Jackson
- frontcode
- clean code
- servlet
- Design Pattern
- EffectiveJava
- code
- JavaScript Core
- toby
- JPA
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
