2023년 1월 1일
08:00 AM
Buffering ...

최근 글 👑

VO, BO, DAO, DTO

2024. 9. 12. 02:48ㆍ정보

하루에 4개씩 반복!

더보기

1.  @Transactional의 동작 원리에 대해 설명해보자!

  • @Transactional을 메서드 또는 클래스에 명시하면, AOP를 통해 Target이 상속하고 있는 인터페이스 또는 Target 객체를 상속한 Proxy 객체가 생성되며, Proxy 객체의 메서드를 호출하면 Target 메서드 전 후로 트랜잭션 처리를 수행합니다.

 


2. @Transactional를 스프링 Bean의 메서드 A에 적용하였고, 해당 Bean의 메서드 B가 호출되었을때, B 메서드 내부에서 A메서드 호출하면 어떤 요청 흐름이 발생하는지 설명해보자..!

 

  • 프록시는 클라이언트가 타겟 객체를 호출하는 과정에만 동작하며, 타겟 객체의 메서드가 자기 자신의 다른 메서드를 호출할 때는 프록시가 동작하지 않는다! 즉, A 메서드는 프록시로 감싸진 메서드가 아니므로 트랜잭션이 적용되지 않은 일반 코드가 수행됩니다.

 


3. A 라는 Service 객체의 메소드가 존재하고, 그 메소드 내부에서 로컬 트랜잭션 3개(다른 Service 객체의 트랜잭션 메소드를 호출했다는 의미)가 존재한다고 할 때, @Transactional을 A 메소드에 적용하면 어떤 요청 흐름이 발생하는지 설명해주세요.

 

  • 트랜잭션 전파 수준에 따라 달라지는데, 만약 기본 옵션인 Required를 가져간다면 로컬 트랜잭션 3개가 부모 트랜잭션인 A에 합류하여 수행됩니다.

 



4. @Transactional에 readOnly 속성을 사용하는 이유에 대해서 설명을 해보자!
 
  • 트랜잭션 안에서 수정/ 삭제 작업이 안니 ReadOnly 목적인 경우에 주로 사용하며, 영속성 컨텍스트에서 엔티티를 관리 할 필요가 없기 때문에 readOnly를 추가하는 것으로 메모리 성능을 높일 수 있고, 데이터 변경 불가능 로직임을 코드로 표시할 수 있어 가독성이 높아진다는 장점이 있다!

  • readOnly 속성이 없는 보통의 트랜잭션은 데이터 조회 결과 엔티티가 영속성 컨텍스트에 관리되며, 이는 1차 캐싱부터 변경 감지(Dirty Checking)까지 가능하게 된다. 하지만, 조회 시 스냅샷 인스턴스를 생성해 보관하기 때문에 메모리 사용량이 증가한다!

 


1. VO (Value Object) 

 

  • "변경 불가능하며 오직 읽기만 가능 (Read-Only)"

  • VO는 시스템에서 하나의 값으로 간주되는 객체이다! 불변성을 유지 (ReadOnly), 값 자체가 중요한 의미를 가진다! 주로 데이터의 동일성을 비교하거나 전송할 때 사용된다!

    • 불변성 유지? 값 자체가 중요한 의미? 그게 뭐지?
      • VO에서 값이 고유한 정보로서 의미를 갖고, 그 값의 변경이나 조작이 불가능하다는 것을 의미합니다(불변성). 예를 들어, 책의 제목이 "노인과 바다" 라고 설정되었을 때, 이 값은 이후 시스템에서 여러 번 사용되지만 그 자체로 변하지 않고 고유한 정보로서 의미를 가집니다. 추가적으로 더 예를 들어보면, "노인과 바다" 라는 제목으로 설정된 또 다른 책이 있다고 할 때, 이 제목이 두 책 모두 동일한 제목이라고 간주할 수 있습니다!(바로 아래 왜 그런지 설명 있습니다!)

  • 그 이유는 바로! 값 기반 비교 때문입니다! 그럼 값 기반 비교란 무엇일까?
    • 객체의 참조가 아닌, 내부의 값이 동일한지 여부로 비교한다! 예를 들어서 두 개의 같은 주소 값이 있을때, 참조(메모리 주소)가 다르더라도 주소가 동일하다면 같은 VO로 인식한다! 

public class BookVO {
    private final String title;  // 책의 제목
    private final String author; // 책의 저자

BookVO는 title과 author 필드를 private final로 선언을 하여 외부에서 직접 접근 하는 것과
한 번 설정된 값은 변경될 수 없게 만들면서 BookVO는 불변 객체가 되도록 설계!

 

public BookVO(String title, String author) {
    this.title = title;
    this.author = author;
}


 생성자를 통해 title과 author 필드를 초기화!

 

public String getTitle() {
    return title;
}

public String getAuthor() {
    return author;
}

getter 메서드를 통해 필드를 직접 노출하지 않고 메서드를 통해 전달해줌으로 캡슐화
VO는 Setter를 제공하지 않는다

 

@Override
public boolean equals(Object o) {
    if (this == o){
    	return true; // 동일한 참조일 경우 바로 true 반환
    }
    if (o == null || getClass() != o.getClass()){
    	return false;  // 타입이 다르면 false
    }
    BookVO book = (BookVO) o;  // Object를 BookVO로 타입 캐스팅
    return title.equals(book.title) && author.equals(book.author); // 값 비교
}


this == o 는 두 객체가 완전히 같은 객체일 경우, 즉 참조값이 동일하면 바로 true를 반환한다! 
어? 근데 VO는 값 기반 비교라며??!!! 고민하지 말자! 여기서 말하는 참조값이라면 메모리 주소인데 
두 객체가 같은 메모리 주소다? 그럼 굳이 비교 할 필요가 있을까? 
어차피 결과는 두 객체가 같은 메모리 주소니까 true인데?

o == null || getClass() != o.getClass() 여기서는 타입 체크를 해서 만약 o가 null이거나 
BookVO 클래스가 아닌 다른 타입이라면 false를 반환하도록 했다. 
쉽게 생각해보자 두 객체가 같은지 비교할려고 하는데 클래스가 다르면 당연히 다르니 비교 할 필요가 없다.

그 다음으로는 Object 타입인 o를 BookVO 타입으로 변환해서 title과 author 값을 각각 비교! 
title과 author가 모두 같으면 두 객체는 같은 것으로 간주하고 true가 반환!

 

@Override
public int hashCode() {
    return Objects.hash(title, author);
}

equals()에서 두 객체를 값 기반 비교한 것처럼, hashCode()도 동일한 필드를 기반으로 해시 코드를 계산하여 
동일한 객체는 동일한 해시 코드를 반환하게 만든다!
이렇게 하면 HashSet, HashMap 등 해시 기반 컬렉션에서 정확한 동작을 보장한다!

 

BookVO book1 = new BookVO("노인과 바다", "어니스트 헤밍웨이");
BookVO book2 = new BookVO("노인과 바다", "어니스트 헤밍웨이");
BookVO book3 = new BookVO("헝거게임", "수잔 콜린스");

비교를 위해 각각 다른 책 객체를 생성
System.out.println(book1.equals(book2)); // true (같은 값이므로 true)
System.out.println(book1.equals(book3)); // false (값이 다르므로 false)

book1.equals(book2)는 true를 반환! 두 객체는 서로 다른 메모리 주소를 가지고 있지만 title과 author 값이
같기 때문에 true를 반환한다!
book2.equals(book3)는 false를 반환! book3의 title과 author 값이 book1과 다르기 때문에 false 반환!

 

  • VO를 사용하면 뭐가 좋을까?
    • VO를 사용하면 코드의 가독성과 유지보수가 쉬워진다!

    • 명확한 의미 전달
      • 예를 들어 BookVO라는 객체를 만들었을 때, 이 객체는 "책 정보를 담는 객체" 라는 의미가 명확하다. 이름을 보면은 아! 이 객체는 책의 제목과 저자 같은 정보를 담고 있겠다 하고 바로 이해 할 수 있듯이 객체가 무슨 역할을 하는지 알 수 있으니, 코드를 작성하거나, 다른 사람이 코드를 읽을 때 쉽게 이해가 가능하다!

    • 응집도 높은 객체
      • 예를 들어 책의 제목과 저자를 별도의 변수로 따로따로 관리한다고 생각을 해보면 코드도 복잡해지고 유지 보수하는데도 불편한 문제점이 생긴다 그런데 VO 객체는 데이터를 하나로 묶어 BookVO라는 객체에 책의 제목과 저자를 함께 담아 두면, 이 정보를 한꺼번에 관리할 수 있다! 이렇게 데이터를 하나의 단위로 묶으면, 데이터 일관성도 유지되고, 관리하기도 쉬워진다!.

  • VO는 비즈니스 로직이 없다??

    • 비즈니스 로직이 없어? 그럼 어떤 역할을 하는걸까? VO는 값을 표현하는 객체! 불변의 특성을 갖고 있으며 비즈니스 로직을 포함하지 않는 것이 일반적인 설계 원칙! 이러한 이유는 특정한 값을 안전하게 나타내기 위한 객체로 사용되기 때문에, 그 안에서의 로직이나 상태 변경을 최소화하고 데이터를 담는 역할에 충실하도록 설계! 

  • DTO와 VO 다른 차이점은 뭐가 있을까?

    • DTO는 setter를 가지고 있어 값을 변경할 수 있지만, VO는 getter만(상황에 따라 setter을 가질 수 있지만 권장되는 설계가 아님)을 가지고 있기 때문에 읽기만 가능하고 수정은 불가능 하다!

    • 간단하게 요약해서 설명하자면 DTO는 단지 데이터를 담아 전달하는 역할을 하지만, VO는 값들에 대해 읽기만 가능한 Read-Only 속성을 가져 객체로서 데이터 자체에 의미를 갖는다!

       

 

2. BO (Business Object) - 비즈니스 객체

 

  • "BO는 핵심 비즈니스 로직을 처리하는 객체"

  • 위에서 VO를 다루어 봤는데 BO는 쉽게 말해서 VO에 없는 비즈니스 로직이 이 들어간 객체로 애플리케이션의 핵심 비즈니스 로직을 처리하는 객체! 이 객체는 주로 서비스 계층에서 사용되고 데이터 처리나 계산을 포함한 비즈니스 로직을 구현한다! 쉽게 풀어서 예를 들어보자!

  • BO 예시) 택배를 예로 들어보면은 고객이 물건을 주문하면, 물류 센터에서 물건을 포장하고 처리해서 고객에세 보낼 준비를 하는데 이 과정에서 어떤 비즈니스 규칙이 적용될지( 포장 방식이나 배송 루트, 배송은 언제까지 도착하는지 등등)이 규칙이  BO가 하는 역할과 비슷하다! (여기서 BO의 특징인 데이터 가공이 해당된다)

  • 비즈니스 로직은 언제든지 변경될 수 있다! 따라서 BO는 그 변화에 대응할 수 있도록 유연하게 설계된다! 

  • 아래 코드를 통해서 BO의 예시 코드를 봐보자
택배를 예시로 코드 작성

//주문 항목 저장 리스트
public class OrderBO {
    private List<OrderItem> items;

	//생성자를 통해 주문 항목 리스트 설정
    public OrderBO(List<OrderItem> items) {
        this.items = items;
    }

리스트를 통해 Item을 필드로 선언.
주문 항목 리스트를 기반으로 이후 로직이 진행

 

 // 비즈니스 로직: 총 물품 무게 계산
 public double calculateTotalWeight() {
    return items.stream()  // 주문 항목 리스트의 모든 항목을 순회
          .mapToDouble(OrderItem::getWeight)  // 각 항목의 무게를 가져옴
          .sum();  // 모든 항목의 무게를 합산
    }
    
 이 코드에서는 calculateTotalWeigh를 통해 물품의 총 무게를 계산하도록 작성
 로직에서 item.stream()은 주문 항목 리스트인 item에 포함된 각 항목을 하나씩 처리하기 위해 stream을 사용
 mapToDouble(OrderItem::getWeight) mapToDouble을 사용해서 특정 속성을 double로 형태를 변환
 OrderItem::getWeight를 사용해서 주문 항목의 무게를 가져오고 double 값으로 변환
 sum을 통해 앞에서 가져온 항목의 무게를 합산

 

 // 비즈니스 로직: 배송비 계산
    public double calculateShippingCost(String shippingMethod) {
        double totalWeight = calculateTotalWeight();  // 먼저 총 무게를 계산

        // 배송 방식에 따라 다른 배송비 계산 로직을 적용
        switch (shippingMethod) {
            case "standard":
                return totalWeight * 5;  // 표준 배송: 무게당 5달러
            case "express":
                return totalWeight * 10;  // 빠른 배송: 무게당 10달러
            default:
                return totalWeight * 3;  // 기본 배송: 무게당 3달러
        }
    }
    
    calculateTotalWeight(); 총 무게를 계산해서 가져온다.
    switch(shippingMethod) 배송 방식에 따라 다른 배송비를 계산
    이렇게 비즈니스 규칙에 따라 다르게 처리하는 로직이 바로 BO의 핵심 역할

 

  • BO를 사용하면 뭐가 좋을까?
    • 비즈니스 로직을 한 곳에 집중시켜 코드의 가독성과 유지보수성을 높인다! 즉, 데이터를 처리하는 핵심 규칙이 흩어져 있지 않고, 한 클래스 내에 모두 포함되어 있어 관리가 쉽고 직관적이다!

    • BO는 데이터를 단순히 전달하는 것이 아니라 비즈니스 규칙에 맞춰 데이터를 가공하고 처리할 수 있다! 즉, 입력된 데이터를 비즈니스 로직을 통해 의미 있는 정보로 변환하거나 계산!

  • 그럼 BO는 언제 사용할까?

    • 복잡한 비즈니스 로직을 처리해야 할 때 사용한다! 또는 데이터를 가공하고 처리해야 할 때! 예를 들어 단순히 데이터를 저장하는 것이 아니라, 계산하거나 규칙을 적용할 때 사용한다!

 


3. DTO(Data Transfer Object) - 데이터 전송 객체

 

  • "계층 간 데이터 교환을 위한 객체"

  • DTO는 데이터를 전송하기 위한 객체! 로직을 가지지 않고 getter/setter 메서드만 가진 순수한 데이터 객체 클래스(Java Beans)로 DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용한다!

  • DTO를 굳이 예를 들자면은 상자랑 비슷하다! 상자를 통해 내용물을 다른 장소로 전달하는 역할! 상자 자체가 상자 안에 있는 내용물을 만지거나 바꾸지 않고 그대로 운반하는 것 처럼 DTO도 데이터를 그대로 안전하게 전송하는 역할!

  • 아래 코드를 통해서 한번 알아보자!
@Getter
@Setter
public class PersonDto{
	private String name;
    private int age;
    private double weight;
    
    public PersonDto(String name, int age, double weight){
    	this.name = name;
        this.age = age;
        this.weight = weight;
    }

@Getter, @Setter 어노테이션을 사용해서 getter,setter 메서드를 자동으로 생성
필드를 private로 설정해 외부에서 필드에 직접 접근하지 못하도록 작성
name,age,weight를 필드로 지정 후 생성자를 통해 필드 값들을 초기화

 

  • 그럼 DTO를 사용하면 뭐가 좋을까?

    • DTO를 사용하므로 계층 간 데이터를 안전하게 전달할 수 있다! 이렇게 데이터를 전달하는 방식은 코드의 의존성을 최소화 하면서, 계층 간 데이터 흐름을 쉽게 관리할 수 있다!

    • DTO는 비즈니스 로직이 없고, 데이터를 담기만 하기 때문에 구조가 간결하다! 따라서 유지보수도 용이하고 어떤 목적을 가지고 있는지도 명확하다!

4. DAO (Data Access Object) - 데이터 접근 객체

 

  • "실제로 DB의 데이터에 접근하는 객체"

  • DAO는 service와 DB를 연결하는 역할을 한다! 실제로 DB에 접근하여 data를 삽입, 삭제, 조회, 수정 등 CRUD 기능을 수행한다!!

  • JPA에서는 DB에 데이터를 CRUD 하는 JpaRepository<>를 상속받는 Repository 객체들이 DAO라고 볼 수 있다!

  • 아래 코드를 통해 한번 알아보자!
public interface TodoRepository extends JpaRepository<Item, Long> {
}

JpaRepository를 상속받는 TodoRepository가 DAO라고 할 수 있다

 

  • DAO의 장점은 무엇일까?

    • DAO는 데이터베이스 작업을 분리하여 비즈니스 로직과 독립적으로 처리할 수 있다 ! 이를 통해서 단일 책임 원칙 (SRP)을 유지하고, 코드의 유지보수성을 높인다!

    • Spring Data JPA를 통해 기본적으로 CRUD 작업을 자동화할 수 있다!

VO, BO, DTO, DAO의 차이점!

 

구분 VO(Value Object) BO(Business Object) DTO(Data Transfer Object) DAO(Data Access Object)
목적 값을 표현하고 동일성 판단 비즈니스 로직을 처리하고 데이터 가공 계층 간 데이터 전송 데이터베이스와 상호작용, CRUD 작업
주요 역할 불변 데이터를 캡술화 및 전달 비즈니스규칙에 따라 데이터를 처리하고 계산 데이터를 다른 계층에 전달하는 역할 데이터 저장, 조회, 수정, 삭제 작업을 담당
불변성 불변 변경 가능 변경 가능 상태 변경 가능
비즈니스 로직 포함 여부 포함하지 않음 포함 (핵심 비즈니스 로직 처리) 포함하지 않음  포함하지 않음
데이터 가공 없음 데이터를 가공하고 처리 없음 없음
주요 사용 메서드 Getter, equals(),
hashCode()
비즈니스 로직 메서드 Getter, Setter CRUD 메서드
상태 변경 불가능  가능 가능 가능
사용 시점 값이 중요한 경우 비즈니스 로직을 처리해야 할 때 데이터 전송이 필요할 때 데이터베이스와 상호작용이 필요할 때
장점 불변성을 통해 데이터 일관성 보장, 값 기반 비교 비즈니스 로직을 한 곳에서 관리, 유지보수 용이 계층 간 데이터 흐름을 안전하게 관리 데이터 접근 로직을 분리하여 유지보수성 향상