ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java/Spring] Entity: 개체
    Spring 2025. 12. 12. 16:13

    자바와 스프링을 공부하는 개발자에게 "Entity(엔티티)가 무엇인가요?"라고 묻는다면, 아마 많은 사람들은 이렇게 대답할 것입니다.

    "아, 그거 JPA 쓸 때 클래스 위에 @Entity 어노테이션 붙이는 거 말하는 거죠? 데이터베이스 테이블이랑 매핑되는 클래스요."

    틀린 말은 아닙니다. 하지만 이것은 'JPA라는 기술 안에서의 Entity'를 설명하는 것일 뿐, 소프트웨어 설계 전반을 관통하는 'Entity의 본질'을 완벽하게 설명했다고 보기는 어렵습니다. 우리는 종종 특정 프레임워크나 라이브러리(ORM)의 사용법에 익숙해진 나머지, 그 기술이 대변하고 있는 본래의 개념을 잊어버리곤 합니다.

    Entity를 단순히 'DB 테이블과 1:1로 매핑되는 껍데기'로만 이해한다면, 비즈니스 로직이 들어갈 자리에 단순한 데이터 홀더(Getter/Setter)만 남게 되고, 결국 객체지향적인 설계와는 멀어지게 됩니다.

    그렇다면 Entity란 도대체 무엇일까요? 이 질문에 제대로 답하기 위해서는 시야를 조금 더 넓혀야 합니다. 단순히 JPA의 구현체를 넘어, 도메인(Domain) 관점데이터베이스(DB) 관점, 그리고 이 둘을 연결하는 JPA 관점을 나누어 살펴볼 필요가 있습니다.

    이번 글에서는 도메인 엔티티(Domain Entity), DB 엔티티(DB Entity), 그리고 JPA 엔티티(JPA Entity) 세 가지 개념을 각각 살펴보고, 이를 통해 우리가 추구해야 할 진짜 Entity의 정의를 내려보도록 하겠습니다.

    1. 도메인 엔티티 (Domain Entity)

    '엔티티'라는 단어의 본질적인 의미를 파악하기 위해서는 가장 먼저 도메인(Domain)이 무엇인지 이해해야 합니다.

    소프트웨어 개발에서 도메인이란 '소프트웨어가 해결하고자 하는 문제의 영역' 또는 '비즈니스 영역'을 의미합니다. 이해를 돕기 위해 병원을 예로 들어보겠습니다. 만약 우리가 병원 운영에 필요한 소프트웨어를 만든다면, 여기서의 도메인은 '병원'이 됩니다. 병원이라는 도메인 안에는 다음과 같은 여러 가지 개념들이 존재할 것입니다.

    • 진료 예약 (Reservation)
    • 외래 환자 (Outpatient)
    • 응급실 (Emergency Room)

    개발자는 이 개념들을 소프트웨어로 옮겨와야 합니다. 즉, 병원이라는 현실 세계의 개념을 코드로 구현하기 위해 클래스(Class)로 정의하게 되는데, 이렇게 만들어진 결과물을 우리는 도메인 모델(Domain Model)이라고 부릅니다. 도메인 모델은 도메인의 문제를 해결하기 위해 설계된 클래스들의 집합입니다.

    도메인 모델 중 '특별한' 존재: 엔티티(Entity)

    그런데 도메인 모델링을 하다 보면, 수많은 모델 중에서 유독 특별한 성격을 띠는 모델들이 나타납니다. 단순히 정보를 담고 있는 것을 넘어서, 다음과 같은 특징을 가진 모델들이죠.

    1. 식별자(Identity)를 갖는다: 다른 객체와 구별되는 고유한 ID가 존재한다.
    2. 비즈니스 로직(Business Logic)을 갖는다: 도메인에 특화된 동작을 수행한다.
    3. 생명주기(Lifecycle)를 갖는다: 생성되고, 변경되고, 소멸되는 과정이 관리된다.

    예를 들어, '진료 예약(Reservation)'이라는 모델을 생각해 봅시다. 예약은 고유한 예약 번호(식별자)가 있어야 하고, 예약 확정이나 취소와 같은 비즈니스 로직이 필요하며, 예약 생성부터 진료 완료까지의 상태 변화(생명주기)를 겪습니다.

    이처럼 도메인 모델 중에서 식별 가능하고, 비즈니스 로직을 가지며, 생명주기를 가진 특별한 모델을 도메인 엔티티(Domain Entity)라고 부릅니다.

    소프트웨어 개발에서의 엔티티

    일반적으로 소프트웨어 공학 이론이나 설계 서적에서 말하는 '엔티티'는 바로 이 도메인 엔티티를 가리킵니다.

    그 이유는 소프트웨어 개발의 목적 자체가 '도메인의 문제를 해결하는 것'이기 때문입니다. 소프트웨어를 개발한다는 것은 도메인을 모델링한다는 뜻이고, 그 모델링의 핵심 결과물이 바로 도메인 엔티티입니다. 따라서 기술적인 구현(DB나 프레임워크)을 떠나, 비즈니스 로직의 주체가 되는 객체가 바로 도메인 엔티티입니다.

     

    2. DB 엔티티 (DB Entity)

    소프트웨어 개발에서 '엔티티'라는 용어가 가장 빈번하게 등장하는 곳 중 하나가 데이터베이스 설계 영역입니다. 하지만 우리가 어떤 종류의 데이터베이스를 사용하느냐에 따라 DB 엔티티가 가지는 형태와 의미는 조금 달라질 수 있습니다.

    여기서는 가장 대중적인 RDBMS(MySQL, PostgreSQL)와 대표적인 NoSQL(MongoDB)의 관점에서 DB 엔티티를 비교해 보겠습니다.

    1) 관계형 데이터베이스 (RDBMS - MySQL, PostgreSQL)

    관계형 데이터베이스 관점에서의 엔티티는 '테이블(Table)'로 정의됩니다.

    • 구조: 사전에 정의된 엄격한 스키마(Schema)를 따릅니다. 엔티티는 테이블이 되고, 속성은 컬럼(Column)이 됩니다.
    • 인스턴스: 테이블에 저장된 하나의 행(Row)이 엔티티의 인스턴스가 됩니다.
    • 관계: 데이터의 중복을 피하기 위해 정규화(Normalization) 과정을 거치며, 쪼개진 엔티티들은 외래 키(Foreign Key)를 통해 연결됩니다.

    예를 들어 '주문(Order)'과 '주문 상품(OrderItem)'이 있다면, RDBMS에서는 이 둘을 별도의 테이블(엔티티)로 분리하고 ID를 통해 참조하는 것이 일반적입니다.

    2) 문서형 데이터베이스 (NoSQL - MongoDB)

    반면, MongoDB와 같은 문서형 데이터베이스(Document DB)에서 엔티티는 '컬렉션(Collection)' 내의 '문서(Document)' 형태로 표현됩니다.

    • 구조: 고정된 스키마가 없거나 유연합니다(Schema-less). 데이터는 JSON과 유사한 BSON 형태로 저장됩니다.
    • 인스턴스: 하나의 문서(Document) 자체가 엔티티의 인스턴스입니다.
    • 관계: RDBMS와 가장 큰 차이점은 '임베딩(Embedding)'입니다. 정규화를 강제하지 않기 때문에, 연관된 데이터를 별도의 엔티티로 나누지 않고 하나의 문서 안에 내장시킬 수 있습니다.

    앞서 든 예시인 '주문(Order)' 안에 '주문 상품(OrderItem)' 리스트를 그대로 포함시켜 하나의 거대한 '주문 문서'로 저장할 수 있습니다. 이 경우 MongoDB 관점에서는 이 전체 문서 하나가 하나의 DB 엔티티로 취급될 수 있습니다.

    DB 엔티티의 공통된 본질

    MySQL의 테이블이든, MongoDB의 문서든 형태는 다르지만 DB 엔티티가 갖는 본질적인 역할은 같습니다.

    1. 영속성(Persistence): 도메인 엔티티의 상태(데이터)를 영구적으로 저장하는 '그릇' 역할을 합니다.
    2. 식별자(ID): RDBMS의 Primary Key나 MongoDB의 _id 처럼, 데이터를 구분하기 위한 고유 식별자를 반드시 가집니다.
    3. 행위의 부재: 이것이 가장 중요합니다. DB 엔티티는 데이터의 저장 구조를 정의할 뿐, 그 자체로 복잡한 비즈니스 로직(행위)을 수행하지 않습니다. 로직은 애플리케이션 코드(도메인 엔티티)의 몫입니다.

    결국 DB 엔티티는 "도메인 로직이 실행된 결과(상태)를 데이터베이스라는 창고에 어떻게 효율적으로 쌓아둘 것인가?"에 대한 해답이라고 볼 수 있습니다.

    3. JPA 엔티티 (JPA Entity)

    우리가 스프링 부트로 프로젝트를 시작할 때 가장 먼저 만드는 클래스, 바로 @Entity 어노테이션이 붙은 클래스가 여기서 말하는 JPA 엔티티입니다.

    앞서 우리는 '도메인 엔티티(비즈니스 개념)'와 'DB 엔티티(물리적 저장소)'를 구분했습니다. 그렇다면 JPA 엔티티는 이 둘 사이에서 어떤 위치에 있으며, 어떻게 정의해야 할까요?

    JPA 엔티티의 본질: 영속성 객체 (Persistence Object)

    결론부터 말하자면, JPA 엔티티는 '영속성 객체'입니다.

    단순히 데이터를 담는 그릇(DTO)이나 비즈니스 로직만 수행하는 일반 객체와는 다릅니다. JPA 엔티티는 영속성 컨텍스트(Persistence Context)라는 환경 속에서 관리되며, 데이터베이스의 상태와 연결되어 있는 객체입니다.

    • Java Persistence API: 기술의 이름에서 알 수 있듯이, 이 객체의 주 목적은 '영속성(Persistence)', 즉 데이터가 프로그램 종료 후에도 사라지지 않고 DB에 영구적으로 저장되도록 하는 것입니다.
    • 생명주기 관리: JPA 엔티티는 생성(New), 관리(Managed), 분리(Detached), 삭제(Removed)라는 고유의 생명주기를 가지며, 이 과정에서 데이터베이스와 끊임없이 소통합니다.

    ORM의 역할: 도메인과 DB의 연결 고리

    JPA는 ORM(Object-Relational Mapping) 기술의 표준입니다. 이름 그대로 객체(Object)와 관계형 데이터베이스(Relational) 사이를 매핑(Mapping) 해주는 기술이죠.

    즉, JPA 엔티티는 도메인 엔티티의 모습을 하고 있지만, 물리적으로는 DB 엔티티(테이블)에 맞춰 구현된 결과물입니다.

    • 도메인 관점: 자바 클래스로 작성되므로 필드와 메서드를 가지며 비즈니스 로직을 수행할 수 있습니다.
    • DB 관점: @Table, @Column 등의 메타데이터를 통해 DB 테이블의 스키마 구조를 그대로 반영합니다.

    JPA 엔티티의 특징 

    '영속성 객체'로서의 역할을 수행하기 위해, JPA 엔티티는 순수한 도메인 모델과는 다르게 프레임워크가 데이터를 핸들링하기 위한 몇 가지 기술적인 제약 사항들을 가집니다.

    1. 어노테이션: @Entity 선언이 필수이며, DB 테이블의 PK와 매핑될 식별자(@Id)가 반드시 있어야 합니다.
    2. 기본 생성자: JPA 구현체(Hibernate 등)가 리플렉션(Reflection) 기술을 사용하여 객체를 프록시(Proxy)로 생성하거나 값을 주입해야 합니다. 따라서 매개변수가 없는 기본 생성자(No-args Constructor)가 반드시 필요합니다. (protected 접근 제어자 이상)
    3. final 제한: 지연 로딩(Lazy Loading)을 위해 실제 객체 대신 가짜 객체(프록시)를 만들어야 할 때가 많습니다. 이를 위해 클래스나 메서드가 final로 선언되어 있으면 상속/오버라이딩이 불가능하므로 피해야 합니다.

    결국 JPA 엔티티는 도메인 로직을 담을 수 있는 그릇이면서, 동시에 DB에 저장되기 위한 영속성 객체라고 이해하면 가장 정확합니다.

     

    3. 엔티티 예시 코드

    도메인 엔티티 (Domain Entity) 예시

    특징: 프레임워크나 DB 기술에 의존하지 않는 순수한 자바 객체(POJO)입니다. 식별자가 있고, 비즈니스 로직(행위)을 가지고 있습니다.

    Java
     
    public class Member {
        // 1. 식별자 (Identity)
        private final Long id;
        private String name;
        private int age;
        private boolean active;
    
        public Member(Long id, String name, int age) {
            if (name == null || name.isBlank()) {
                throw new IllegalArgumentException("이름은 필수입니다.");
            }
            this.id = id;
            this.name = name;
            this.age = age;
            this.active = true;
        }
    
        // 2. 비즈니스 로직 (행위, Behavior)
        // 단순 Setter가 아니라 의미 있는 메서드를 제공합니다.
        public void changeName(String newName) {
            if (newName == null || newName.length() < 2) {
                throw new IllegalArgumentException("이름은 2글자 이상이어야 합니다.");
            }
            this.name = newName;
        }
    
        public void deactivate() {
            this.active = false;
        }
    
        // Getter (상태 확인용)
        public Long getId() { return id; }
        public String getName() { return name; }
        public boolean isActive() { return active; }
        // Setter는 남발하지 않습니다.
    }
    

    설명: 어떠한 어노테이션도 붙어있지 않습니다. 오로지 '회원'이라는 도메인의 규칙(이름 검증 등)과 상태 변경에만 집중합니다.

    DB 엔티티 (DB Entity) 예시

    특징: 데이터가 저장될 물리적인 테이블(Table) 구조입니다. 행위(메서드)는 없고 오직 데이터의 타입과 제약조건(Schema)만 존재합니다.

    (SQL DDL 예시)

     
    CREATE TABLE member (
        -- 1. 식별자 (PK)
        member_id BIGINT AUTO_INCREMENT PRIMARY KEY,
        
        -- 2. 상태 (Data)
        name VARCHAR(255) NOT NULL,
        age INTEGER,
        is_active BOOLEAN DEFAULT TRUE,
        
        -- 3. 메타데이터 (생성일, 수정일 등)
        created_at DATETIME,
        updated_at DATETIME
    );
    

    설명: DB 엔티티는 '코드'라기보다는 '스키마'입니다. 비즈니스 로직은 전혀 없으며, 오직 데이터를 효율적으로 저장하기 위한 구조만 정의되어 있습니다.

    3. JPA 엔티티 (JPA Entity) 예시

    특징: 도메인 엔티티의 모습을 하고 있지만, DB 테이블과 매핑하기 위한 기술적인 장치(어노테이션, 기본 생성자 등)가 포함된 영속성 객체입니다.

     
    import jakarta.persistence.*;
    
    @Entity // JPA 엔티티임을 명시
    @Table(name = "member") // DB 엔티티(테이블)와 매핑
    public class MemberJpaEntity {
    
        @Id // 식별자 매핑
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "member_id")
        private Long id;
    
        @Column(nullable = false)
        private String name;
    
        private int age;
    
        @Column(name = "is_active")
        private boolean active;
    
        // [기술적 제약 1] 기본 생성자 필수 (Protected 권장)
        // JPA가 프록시 기술을 쓰기 위해 필요함
        protected MemberJpaEntity() {
        }
    
        // 편의를 위한 생성자
        public MemberJpaEntity(String name, int age) {
            this.name = name;
            this.age = age;
            this.active = true;
        }
    
        // [선택 사항] 실무에서는 편의상 여기에 도메인 로직을 넣기도 함
        public void updateName(String name) {
            this.name = name;
        }
        
        // Getters...
    }
    

    설명:

    • @Entity, @Id, @Table: DB 엔티티와의 연결 고리입니다.
    • protected MemberJpaEntity() {}: 순수 도메인 모델에는 필요 없는, 오직 JPA 기술을 위한 코드입니다.
    • 실무적으로는 이 클래스 안에 도메인 로직(updateName)을 함께 작성하여 도메인 엔티티 + JPA 엔티티 역할을 동시에 수행하게 하는 경우가 많습니다.

    4. 결론: 그래서 엔티티(Entity)가 무엇인가요?

    지금까지 우리는 엔티티를 도메인(비즈니스 문제), DB(데이터 저장), JPA(영속성 기술)라는 세 가지 시선으로 바라보았습니다.

    • 도메인 엔티티: 문제를 해결하기 위해 식별자와 로직을 가진 핵심 모델
    • DB 엔티티: 데이터가 영구적으로 저장되는 물리적 구조 (테이블, 문서)
    • JPA 엔티티: 도메인 객체를 DB에 저장하기 위해 매핑된 영속성 객체

    그렇다면 이 관점들을 모두 관통하는 '엔티티의 범용적인 정의'는 과연 무엇일까요?

    "식별자를 가지며, 시간이 지남에 따라 상태가 변하는 객체"

    소프트웨어 세상에서 엔티티를 한 문장으로 정의하자면 위와 같습니다. 이 정의에는 두 가지 핵심 키워드가 숨어 있습니다.

    1. 식별자 (Identity) 엔티티는 속성(값)이 같더라도 서로 다른 객체로 구별될 수 있어야 합니다. 예를 들어, 이름이 '김철수'이고 나이가 '30세'인 사람이 두 명 있다고 가정해 봅시다. 이 둘은 속성(이름, 나이)은 같지만 엄연히 다른 사람입니다. 주민등록번호나 회원 ID 같은 고유한 식별자가 있기 때문입니다. 이것이 바로 속성 값이 같으면 같은 객체로 취급하는 VO(Value Object)와 가장 큰 차이점입니다.

    2. 상태의 변화 (Mutability & Lifecycle) 엔티티는 태어나고(생성), 변하고(수정), 사라지는(삭제) 생명주기(Lifecycle)를 가집니다. 시간이 흘러 '김철수'의 나이가 31세가 되고 개명을 해서 이름이 바뀌더라도, 그 사람은 여전히 같은 사람(식별자 유지)입니다. 즉, 엔티티는 내부의 상태(데이터)가 계속해서 변할 수 있는 존재입니다.

    마무리하며

    우리는 종종 스프링 개발을 하면서 무의식적으로 @Entity를 붙이고 getter/setter를 남발하곤 합니다. 하지만 이제는 조금 다르게 접근해 보는 건 어떨까요?

    기술적인 구현(JPA)이나 저장소의 형태(DB)를 먼저 고민하기 전에, "이 객체가 우리의 비즈니스에서 고유하게 식별되어야 하는가?", "시간이 지남에 따라 상태가 변해야 하는가?"를 먼저 고민해 보세요.

    그 고민 끝에 만들어진 엔티티야말로, 단순히 DB에 데이터를 저장하는 것이 아니라 비즈니스의 핵심 가치를 담은 진정한 의미의 객체가 될 것입니다.

Designed by MSJ.