객체지향 프로그래밍을 한다면 꼭 접하게 되는 Entity, DTO, DAO, VO에 대하여 정리해본다.
Entity
Entity는 시스템에서 관리하는 데이터의 실제 객체를 의미한다.
DB 테이블과 1:1 매핑되는 객체로 시스템의 핵심 데이터와 관련된 정보를 저장하고 관리하는 역할을 하고,
DB와의 상호작용을 위해 사용되며
고유한 식별자(주로 ID)가 존재한다.
비즈니스 로직을 처리하기도 한다.
Entity 클래스에서 비즈니스 로직을 처리하는 경우에 대해서 좀 더 구체적으로 이야기 해보자면..?
우선 Entity에서 비즈니스 로직을 처리하는 이유는 아래와 같다.
1. 일관성 유지
entity 자체가 자신의 상태를 관리할 경우에는 비즈니스 로직을 처리할 수 있다.
예를 들어, 특정 상태에서는 값이 변경되면 안 되는 경우 entity 내 규칙을 적용하는 경우다.
2. 응집도
entity는 해당 객체에 관련된 데이터를 다루고, 그 데이터에 관련된 규칙을 함께 처리하는 것이 응집도가 높아지는 설계일 수 있다.
데이터와 관련된 비즈니스 로직을 함께 두는 것이 객체 지향 설계의 핵심
3. 도메인 주도 설계(Domain-Driven Design)
DDD에서는 도메인 모델(entity)에 비즈니스 로직을 포함시키는 것이 자연스러운 설계로, 도메인 로직을 비즈니스 요구 사항과 밀접하게 결합함으로써 코드의 의미와 가독성을 높일 수 있기 때문이다.
주문 시스템을 예시로 들어보자.
주문에서 최소 주문 금액을 충족해야 결제 처리가 가능하다는 규칙이 있다고 가정해 보고 코드를 살펴보자.
1.1 Order Entity
public class Order {
private Long id;
private Customer customer;
private List<Item> items;
private BigDecimal totalAmount;
private boolean isPaid;
// Constructor
public Order(Customer customer) {
this.customer = customer;
this.items = new ArrayList<>();
this.totalAmount = BigDecimal.ZERO;
this.isPaid = false;
}
// 주문에 아이템 추가
public void addItem(Item item) {
this.items.add(item);
this.totalAmount = this.totalAmount.add(item.getPrice());
}
// 비즈니스 로직: 최소 주문 금액 확인
public void checkout() {
if (this.totalAmount.compareTo(BigDecimal.valueOf(100)) < 0) {
throw new IllegalArgumentException("주문 금액이 최소 금액을 충족하지 않습니다.");
}
this.isPaid = true; // 결제 완료
}
// Getters and Setters
}
1.2 Item Entity
public class Item {
private Long id;
private String name;
private BigDecimal price;
// Constructor
public Item(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
// Getter and Setter
}
1.3 Order 관련 서비스 계층
public class OrderService {
public void processOrder(Order order) {
// 주문에 아이템 추가
Item item1 = new Item("Laptop", BigDecimal.valueOf(50));
Item item2 = new Item("Phone", BigDecimal.valueOf(60));
order.addItem(item1);
order.addItem(item2);
// 주문 결제 처리 (비즈니스 로직이 Entity에 있음)
try {
order.checkout(); // 주문 금액 확인 및 결제 처리
System.out.println("주문이 완료되었습니다.");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 최소 금액 미만 시 예외 처리
}
}
}
주문 Entity(Order)에서 checkout() 메서드는 비즈니스 로직을 포함한다. 이 메서드는 주문 금액이 최소 금액(100)을 초과했는지 확인하고, 이를 만족하지 않으면 예외를 던진다.
주문 Entity는 자신이 아이템을 추가하는 기능(addItem)을 가지고 있으며, 주문 금액을 계산하는 로직과 주문 결제 처리 로직도 가지고 있다. 즉, Entity가 자신의 상태를 일관되게 유지하고 관리하는 역할을 한다.
-> Order Entity는 주문과 관련된 데이터를 다루고 있다. 그 데이터를 기반으로 결제 처리 로직 및 금액 검증 로직을 수행하는 로직은 Order 도메인에 맞는 비즈니스 로직이므로 주문 Entity 내에 두는 것이 자연스럽다.
다만, 모든 비즈니스 로직을 Entity에 두는 것이 항상 좋은 설계는 아니다.
Entity에 로직을 두는 이유와 그 방식에 대해서는 신중해야 하는데….
Entity가 비즈니스 로직을 포함하는 것이 객체 지향 설계의 원칙에 맞는 경우도 있지만, 로직이 너무 복잡해지면 단일 책임 원칙(SRP)을 위반할 수 있기 때문에 Entity가 자신과 관련된 기본적인 비즈니스 규칙을 다루도록 하되, 다른 복잡한 처리 로직은 서비스 계층이나 다른 객체로 분리하는 것이 좋겠다. ( 결국 뭐든 적당한게 좋다,, )
DTO (Data Transfer Object)
DTO는 데이터 전송 객체로, 계층 간 서비스 계층이나 데이터베이스 계층의 데이터 구조에 의존하지 않고 필요에 맞게 변환된 데이터를 제공하기 위해 사용하는 객체다.
클라이언트와 서버 간 데이터 전송 시, 네트워크 오버헤드를 줄이기 위해 데이터의 형태를 간소화하거나 특정 필드만 포함시킬 수 있다.
주로 서비스 계층과 프레젠테이션 계층 (예: 웹, 모바일 앱) 간의 데이터 전송에 쓰인다. -> 네트워크 전송 시 필요한 데이터만 포함하므로, 네트워크 전송 시 불필요한 데이터를 보내지 않아 효율적이다.
DTO는 데이터베이스 Entity와는 별개로(Entity와는 달리, 데이터베이스와의 직접적인 매핑 없이 전송 목적에 맞는 데이터만 포함된다.) 필요에 따라 데이터를 변형하거나 필터링하여 네트워크를 통해 전달되는 데이터를 최적화한다.
내부 DB 데이터 구조가 바뀌어도 DTO만 적절히 수정하여 외부와의 계약을 유지가 가능하다는 장점이 있다. (DB 모델에 대한 의존성 낮음)
Entity에는 민감한 정보가 포함될 수 있고, DTO를 통해 필요한 정보만 전달하므로 보안상 유리하다는 점도 장점이다.
DTO는 비즈니스 로직은 포함하지 않는다.
왜 DTO는 비즈니스 로직을 포함하지 않을까?
비즈니스 로직이 DTO에 포함되면, 비즈니스 로직을 변경할 때마다 DTO 클래스를 수정해야 하므로 유지보수가 복잡해진다.
비즈니스 로직을 포함하게 되면 DTO의 역할이 혼잡해져 응집도가 떨어진다.
DTO는 단순히 데이터를 담고 있는 객체로서의 역할에 집중하게 되면필요한 데이터를 전달하는 역할만 하므로 같은 비즈니스 로직을 여러 DTO에서 공유할 수 있다.
-> 결국 DTO와 비즈니스 로직을 분리하게 되면 각각 재활용성과 응집성을 높일 수 있기 때문..!
// OrderDTO
public class OrderDTO {
private Long orderId;
private String customerName;
private BigDecimal totalPrice; // 계산된 가격
private Integer totalItems; // 계산된 총 아이템 수
// Constructor
public OrderDTO(Long orderId, String customerName, BigDecimal totalPrice, Integer totalItems) {
this.orderId = orderId;
this.customerName = customerName;
this.totalPrice = totalPrice;
this.totalItems = totalItems;
}
// getters and setters
}
// OrderService
public class OrderService {
private OrderDAO orderDAO;
public OrderDTO getOrderDetails(Long orderId) {
// 데이터베이스에서 주문 Entity를 조회
Order order = orderDAO.findById(orderId);
// 비즈니스 로직은 서비스 계층에서 처리
BigDecimal totalPrice = calculateTotalPrice(order);
Integer totalItems = order.getItems().size();
// 계산된 값을 DTO에 담아서 반환
return new OrderDTO(order.getId(), order.getCustomer().getName(), totalPrice, totalItems);
}
private BigDecimal calculateTotalPrice(Order order) {
return order.getItems().stream()
.map(Item::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
VO (Value Object)
VO는 값을 표현하는 객체로, 한 번 생성된 VO 객체는 값이 변경되지 않는 객체다.
데이터베이스와의 연관이 적고, Entity 내에서 도메인 개념을 표현할 때 주로 사용된다.
보통 비교와 같은 작업을 위한 equals(), hashCode() 메소드가 구현된다.
VO를 Entity에 포함하는 경우가 잘 와닿지 않아서 예시를 찾았다.
주문 시스템 구현한다고 가정해보자.
Address라는 VO를 만든다. 주소는 Street, City, PostalCode, Country로 구성된다.
1.1 Address VO
public class Address {
private final String street;
private final String city;
private final String postalCode;
private final String country;
public Address(String street, String city, String postalCode, String country) {
if (street == null || city == null || postalCode == null || country == null) {
throw new IllegalArgumentException("주소의 모든 필드는 null일 수 없습니다.");
}
this.street = street;
this.city = city;
this.postalCode = postalCode;
this.country = country;
}
// Getters
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getPostalCode() {
return postalCode;
}
public String getCountry() {
return country;
}
// equals(), hashCode(), toString()을 적절히 오버라이드 해야 함
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Address address = (Address) obj;
return street.equals(address.street) &&
city.equals(address.city) &&
postalCode.equals(address.postalCode) &&
country.equals(address.country);
}
@Override
public int hashCode() {
return Objects.hash(street, city, postalCode, country);
}
@Override
public String toString() {
return street + ", " + city + ", " + postalCode + ", " + country;
}
}
1.2 Order Entity에 VO 넣기
주문(Order) Entity에서 Address VO를 포함해보자. 주문에 배송 주소를 VO로 표현한다.
public class Order {
private Long id;
private Customer customer;
private List<Item> items;
private BigDecimal totalAmount;
private boolean isPaid;
private Address shippingAddress; // VO로 주소를 포함
public Order(Customer customer, Address shippingAddress) {
this.customer = customer;
this.shippingAddress = shippingAddress;
this.items = new ArrayList<>();
this.totalAmount = BigDecimal.ZERO;
this.isPaid = false;
}
// 아이템 추가
public void addItem(Item item) {
this.items.add(item);
this.totalAmount = this.totalAmount.add(item.getPrice());
}
// 결제 처리
public void checkout() {
if (this.totalAmount.compareTo(BigDecimal.valueOf(100)) < 0) {
throw new IllegalArgumentException("주문 금액이 최소 금액을 충족하지 않습니다.");
}
this.isPaid = true; // 결제 완료
}
// 배송 주소 변경
public void changeShippingAddress(Address newAddress) {
this.shippingAddress = newAddress;
}
// Getters and Setters
public Address getShippingAddress() {
return shippingAddress;
}
}
1.3 Item Entity (아이템)
아이템은 금액과 제품 정보를 갖는 단순한 Entity다.
public class Item {
private Long id;
private String name;
private BigDecimal price;
// Constructor
public Item(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
// Getter and Setter
public BigDecimal getPrice() {
return price;
}
public String getName() {
return name;
}
}
1.4 OrderService (서비스 계층)
서비스 계층에서는 주문생성-아이템 추가-결제 처리를 한다.
public class OrderService {
public void processOrder() {
// 주소 VO 생성
Address shippingAddress = new Address("123 Main St", "Seoul", "12345", "Korea");
// 주문 생성
Order order = new Order(new Customer("John Doe"), shippingAddress);
// 아이템 추가
Item item1 = new Item("Laptop", BigDecimal.valueOf(50));
Item item2 = new Item("Phone", BigDecimal.valueOf(60));
order.addItem(item1);
order.addItem(item2);
// 주문 결제 처리
try {
order.checkout(); // 결제 처리
System.out.println("주문이 완료되었습니다.");
System.out.println("배송 주소: " + order.getShippingAddress());
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
// 배송 주소 변경
Address newAddress = new Address("456 New St", "Busan", "67890", "Korea");
order.changeShippingAddress(newAddress);
System.out.println("새로운 배송 주소: " + order.getShippingAddress());
}
}
Address VO는 street, city, postalCode, country라는 값들을 가지고 불변 객체로 만들어 주소 값이 바뀔 수 없도록 하고, 모든 값은 생성 시 초기화된다. equals, hashCode 메서드를 오버라이드하여 두 주소 객체가 동일한지를 값 기준으로 비교한다.
Order Entity는 shippingAddress라는 속성으로 Address VO를 포함하고 있다. 주문이 생성될 때 배송 주소를 전달하고, 이후 배송 주소를 변경하는 메서드(changeShippingAddress)도 제공한다.
위의 예시처럼 개발하면 (Entity에서 VO를 포함)하면 좋은 점은,
1. VO는 생성된 후 값을 변경할 수 없으므로, 값 객체를 Entity에 포함시키면 해당 Entity의 상태가 일관성을 유지할 수 있다.
2. 주소와 같은 값을 VO로 만들면, 주소에 관련된 유효성 검사를 VO 내부에서 처리하고 Entity는 비즈니스 로직에만 집중할 수 있다.
DAO (Data Access Object)
DAO는 DB와의 상호작용을 담당하는 객체(CRUD (Create, Read, Update, Delete) 작업을 처리) 이다.
비즈니스 로직과 DB 관련 로직을 분리하는 역할로, DAO는 비즈니스 로직을 포함하지 않는다.
public class UserDAO {
public User findById(Long id) {
// DB에서 User를 찾아 반환하는 로직
}
public void save(User user) {
// DB에 User를 저장하는 로직
}
}
나는 entity가 DB와 매핑되는 객체라면 굳이 Dao를 만들어서 할 필요가 있나?라는 궁금증이 생겨서 추가로 entity와 dao를 분리하는 이유를 찾아봤다.
entity와 dao를 분리하는 이유
책임 분리 (Separation of Concerns) - 단일책임의 원칙
Entity는 데이터베이스 테이블과 매핑되는 객체로 비즈니스 도메인 데이터를 저장하는 역할을 한다.
주로 데이터 구조를 정의하고 이를 객체 지향적으로 표현하는 역할이다.
DAO는 데이터베이스와의 상호작용을 담당하는 객체다.
실제로 데이터베이스에서 데이터를 읽고 쓰고 수정하는 등의 CRUD 작업을 처리한다.
각 클래스가 한 가지 책임만 가지도록 해서 코드가 더 명확하고 이해하기 쉽게 처리할 수 있다.
비즈니스 로직과 DB 로직 분리
Entity 클래스는 데이터 구조와 함께 해당 데이터를 어떻게 처리할지에 대한 규칙을 정의하기 때문에 비즈니스 로직을 포함할 수 있다.
DAO는 비즈니스 로직과는 별개로 DB에 대한 세부적인 로직만 담당한다.
비즈니스 로직과 DB로직을 분리하면 각 영역에 대한 수정이 독립적으로 이루어질 수 있다. DB 접근 방식이나 쿼리 방식을 변경한다고 해도 비즈니스 로직에 영향을 미치지 않도록 하는 것이다.
테스트 용이성 (Testability)
DAO는 데이터베이스와의 상호작용을 포함하기 때문에 테스트할 때 실제 DB를 사용하는 것이 어려울 수 있다.
DAO가 Entity와 분리되어 있으면 DB 관련 부분만 모의 객체(Mock 객체)로 대체하여 유닛 테스트를 진행할 수 있다.
Entity는 주로 데이터 저장과 처리에만 집중하기 때문에, 비즈니스 로직 테스트를 독립적으로 수행 가능하다.
유지보수 및 확장성 (Maintainability & Scalability)
Entity와 DAO가 하나의 클래스에 혼합되어 있으면 나중에 데이터베이스 접근 방식이나 구조를 변경할 때 Entity 구조를 수정하거나 DAO를 수정하는 데 혼란이 생길 수 있다.
간혹 ORM 프레임워크를 바꾼다거나 쿼리 방식을 변경할 때 코드의 여러 군데를 수정해야 하는 경우가 생기는 불상사를 방지할 수 있다.
DAO와 Entity를 분리하면 데이터베이스 접근 방식이나 저장소가 변경되더라도 비즈니스 로직에는 영향을 주지 않도록 할 수 있기 때문에 확장성이 향상되며, 새로운 저장소나 데이터베이스 구조를 쉽게 도입할 수 있다.
DAO는 다양한 종류의 DB저장소에 대해 작업을 처리할 수 있다.
RDBMS, NoSQL, 파일 시스템 등 다양한 저장소를 사용할 때 DAO를 분리하고 여러 저장소에 대해 같은 비즈니스 로직을 사용할 수 있도록 한다.
Entity는 각 데이터 저장소에 맞게 적절한 구조를 가질 필요가 있지만, DAO가 이를 추상화하면 데이터 저장소의 종류에 관계없이 동일한 비즈니스 로직을 유지할 수 있다.
또 Entity는 ORM(Object-Relational Mapping) 프레임워크 (예: JPA, Hibernate)에서 사용되는 객체로, 객체와 데이터베이스 간의 매핑을 담당한다. 이 때 DAO는 데이터베이스 쿼리와 연관된 로직을 처리하고 ORM에서 제공하는 기능을 활용한다.
장황하게 썼지만, 결국 변경에 대한 대응이 용이해지기 위해서다.
개발을 하다보면 정말 바뀌는 경우가 많기 때문에,,
// Entity 클래스 (User)
public class User {
private Long id;
private String name;
private String email;
// 비즈니스 로직을 포함할 수 있음
public void updateEmail(String newEmail) {
this.email = newEmail;
}
}
// DAO 클래스 (UserDAO)
public class UserDAO {
private EntityManager entityManager;
// 조회
public User findById(Long id) {
return entityManager.find(User.class, id);
}
// 저장
public void save(User user) {
entityManager.persist(user);
}
// 삭제
public void delete(User user) {
entityManager.remove(user);
}
}
구분 | Entity | DTO | VO | DAO |
주요 역할 | DB 테이블과 매핑 | 계층 간 데이터 전송을 위한 객체 | 값 객체, 불변 객체로 데이터를 표현 | 데이터베이스와의 상호작용 처리 |
주요 특징 | DB와 1:1 매핑, 비즈니스 로직 포함 가능 | 데이터 전송 목적, 비즈니스 로직 없음 | 불변 객체, ID 없음, 동일 값이면 동일 객체, 엔티티의 속성으로 사용 | SQL/ORM 활용, DB의 CRUD 작업 수행 |
변경 가능성 | 변경 가능 | 변경 가능 | 불변 | 변경 가능 |
'프로그래밍 > Java' 카테고리의 다른 글
[Java] Map 개념, HashMap과 ConcurrentHashMap 비교 (0) | 2024.04.25 |
---|