DAO, DTO/Entity/Domain, VO

박영준·2022년 11월 29일
0

Java

목록 보기
12/111

1. DAO (Data Access Object, repository)

1) 정의

// JPA 사용 시
public interface QuestionRepository extends CrudRepository<Question, Long> {
}
  • 실제로 DB에 접근하는 객체
    → Persistence Layer : DB에 data를 CRUD하는 계층

  • Service 와 DB 를 연결하는 고리의 역할

  • SQL 을 사용해서(개발자가 직접 코딩), DB에 접근한 후 적절한 CRUD API 를 제공

2) Controller 에서 Repository 호출

이런식으로 Controller 에서 Repository 를 호출하게 되면, 중복 코드가 발생하게 된다.

결론
Controller 에서 Repository 를 호출하지 말자.

2. Domain/Entity/DTO

1) Domain

온라인서점 사이트에서 책을 조회하고 구매한다고 가정해보자.

'온라인 서점'
--> 구현해야할 소프트웨어의 대상
--> 상품의 조회, 구매, 결제... 등의 기능을 제공

따라서, 온라인 서점은 소프트웨어로 해결하고자하는 문제 영역
= Domain(도메인)

도메인 모델
도메인 모델은 기본적으로 도메인 자체를 이해하기 휘한 개념모델이다.
그러므로 굳이 특정한 형태로 모델을 만들 필요는 없다.

  • 도메인 모델 中 객체를 이용한 모델 -
    이런 도메일 모델을 바탕으로 코드를 작성한다.

즉,
도메인 모델(객체): 내가 개발하고자 하는 영역을 분석하고, 그 분석의 결과로 도출된 모델(객체)

이렇게 도출한 도메인 모델은 크게 Entity와 Value로 구분할 수 있다.

2) Entity

(1) 정의

  • 'Entity 클래스' or '가장 core한 클래스' 라고 부름

  • Entity는 식별자를 가진다.

    Purchase는 주문id를 식별자로 가진다고 가정해보자.
    Purchase에서 주문상태를 변경한다고해서 다른 주문이 되지는 않는다.

    즉, 식별자 외 데이터(주문상태)가 변경된다고 해서
    그 객체가 다른 객체가 되지않는다.

    참고: 식별자

  • Entity 클래스는 실제 DB 테이블과 매핑되는 핵심 클래스
    → DB 테이블에 존재하는 컬럼들을 필드로 가지는 객체
    (DB의 테이블과 1:1로 매핑되며, 테이블이 가지지 않는 컬럼을 필드로 가져서는 안 된다.)

  • Entity 는 DB 영속성(persistent)의 목적으로 사용되는 객체
    → 요청(Request)이나 응답(Response) 값을 전달하는 클래스로 사용해서는 X

  • 서비스 클래스와 비즈니스 로직들이 Entity 클래스를 기준으로 동작하므로, Entity 클래스가 변경되면 여러 클래스에 영향을 줄 수 있다.

(2) 사용법

사용법 1

@Entity
@Data
@NoArgsConstructor
@Table(name = "legacy_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
    
    @Column(name="crtdat", nullable = false)
    private LocalDateTime createdAt;
}

@Entity

  • JPA 로 DB 를 사용하기 위해, 엔티티를 테이블로 지정해줄 때의 필수값

  • DB 의 테이블과 1:1로 매칭되는 객체 단위
    → Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미

  • 기본 생성자는 필수

    • 생성자가 하나도 없으면 자바가 만들어주겠지만,
    • 파라미터가 있는 생성자가 1개라도 있다면 기본생성자를 만들어주지 않는다.
  • final 클래스, enum(enumerated type, 열거형), interface 등...에는 사용 X

  • 저장할 필드라면 final 사용 X

  • 주의!
    이거 자체가 DB 테이블은 아니다.
    자바의 객체일 뿐!

@GeneratedValue

자동으로 기본키를 생성하는 방법에는 4가지가 있다.
→ 기본키를 자동으로 생성할 때에는 @Id와 @GenerratedValue 어노테이션이 함께 사용되어야 한다.

  1. GenerationType.AUTO

    • @GeneratedValue 뒤에 strategy를 설정하지 않으면, GenerationType.AUTO 이 자동 설정된다.
    • null이면 안된다.
    • 유일하게 식별할 수 있어야한다.
    • 변하지 않는 값이어야 한다.
  2. GenerationType.IDENTITY

    • 각 테이블마다 ID값을 개별적으로 사용
    • AUTO_INCREASE를 활용하여 ID를 증가시킨다
    • MYSQL에서 사용 多
    • 현업에서 사용 多
  3. GenerationType.SEQUENCE

    • "시퀀스 전략"
    • 테이블에는 not null로만 들어간다
    • DB에 save()로 저장하기 전에, call next value for hiberante_sequence 전략을 사용해서 1씩 증가시킨다
      → 모든 테이블을 대상으로 1씩 증가
    • @SequenceGenerator 어노테이션이 필요
    • 데이터 베이스의 Sequence Object를 사용하여 데이터베이스가 자동으로 기본키를 생성
  4. GenerationType.TABLE

    • (키를 생성하는 테이블을 사용하는 방법으로 GenerationType.SEQUENCE 와 유사)
    • @TableGenerator 어노테이션이 필요

@Id

  • 직접 기본키를 생성하는 방법
  • JPA 로 DB 를 사용하기 위해, 엔티티를 테이블로 지정해줄 때의 필수값
  • 보통 1씩 증가하는 Long 타입을 사용하여, 유일성을 보장

@Table

  1. name

    • 실제 DB 에서 사용할 테이블 이름을 지정할 수 있다
      → @Table 로 legacy_user를 설정하면, 테이블은 User가 아닌 legacy_user 로 생성된다.
  2. index, uniqeConstraints 등... 그 외의 설정
    JPA 로 테이블을 생성한다면, 적용이 되지만
    기존 DB 테이블이 존재할 경우, indexes와 uniqueConstraints는 동작하지 X

    즉,
    기존 DB 테이블에서 해당 설정이 되어 있으면 작동하지만
    없다면 전혀 적용되지 X

    일반적으로 실제 운영에서는 해당 사항은 DB 설정으로만 남기고, 따로 @Table 에서는 설정하지 않는다.

  3. 생략 시, 매핑한 Entity 이름을 Table 이름으로 사용

@Column

  • 객체 필드를 테이블 컬럼에 매핑하는데 사용

  • 생략 가능: 이름을 지정 할 때 아니고는 보통 생략함

  • 속성

    • name

      • 多 사용
      • 필드와 매핑할 테이블 컬럼 이름
    • nullable (DDL) --> 多 사용

      • null 값의 허용 여부 설정

      • false 설정 시, not null

      • @Column 사용 시, nullable = false 로 설정하는 것이 안전

        Optional 과 기본적인 개념은 동일하다!

    • unique (DDL)

      • @Table의 uniqueConstraints와 같지만, 한 컬럼에 간단히 유니크 제약조건을 적용
    • columnDefinition (DDL)

      • 데이터베이스 컬럼 정보를 직접 줄 수 있음
      • default 값 설정
      • default. 필드의 자바 타입과 방언 정보를 사용해 적절한 컬럼 타입을 생성
    • length (DDL)

      • 문자 길이 제약조건
      • String 타입에만 사용
    • percision, scale (DDL)

      • BigDecimal, BigInteger 타입에서 사용
      • 아주 큰 숫자나 정밀한 소수를 다룰 때만 사용
  • DB 테이블과 Spring에서 사용하는 칼럼명이 다를 경우, @Column 을 사용할 수 있다

  1. name
    내가 Spring에서 사용하고 싶은 칼럼명을 정의할 수 있다

  2. nullable

    • default 가 nullable = true 이므로
      값을 입력하고 싶다면 한다면, @Column(nullable = false)로 설정하여 null이 입력되면 오류가 나도록 설정할 수 있다.

    • nullable = false인 칼럼이 null로 DB에 들어간다면, 다음과 같이 not-null 칼럼이 null을 참조한다는 예외가 나옴

      org.springframework.dao.DataIntegrityViolationException: not-null property references a null 
      or transient value : com.example.demo.domain.User.createdAt; nested exception is 
      org.hibernate.PropertyValueException: not-null property references a null or transient value : 
      com.example.demo.domain.User.createdAt

사용법 2 : Enum

public Enum Gender {
 MALE,  // 0
 FEMALE // 1
}

// 순서 변경할 경우
public Enum Gender {
 FEMALE,  // 0
 MALE     // 1
}

// 중간에 데이터 추가할 경우
public Enum Gender {
 FEMALE,  // 0
 MIX,     // 1
 MALE     // 2
}
  • Enum을 사용하려면 @Enumerated(EnumType.STRING) 를 사용해야 한다.
  • default가 @Enumerated(EnumType.ORIDNAL)
    → "Enum 에 정의된 순서로 값을 구분한다."는 의미
    → 따라서, 중간에 데이터가 추가 or 순서가 바뀌면, 데이터가 꼬여버림

사용법 3 : 생성/수정 시간

@EnableJpaAuditing		// @EnableJpaAuditing을 통해, 해당 프로젝트가 Audit 기능을 사용을 설정
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

생성날짜, 수정날짜가 담긴 클래스 생성 & @CreatedDate와 @LastModifiedDate 설정

@Getter
@MappedSuperclass		
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseEntity {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
  • 각 엔티티에 공통으로 필요한 생성시간, 수정시간, 작성자, 수정자를 공통 클래스로 처리할 수 있다.

@MappedSuperclass

  • 상속받는 자식 클래스에 매핑 정보를 제공하는 역할
  • @Entity는 @Entity나 @MappedSuperclass로 정의된 클래스만 상속받을 수 있다.

@EntityListeners

@Entity나 @MappedSuperclass가 설정된 클래스에 리스너를 추가해줌

@Enumerated

  • Java Enum을 테이블에서 사용한다고 간주하기

  • 속성

    • Ordinal
    • String --> 多 사용
      • 해당 문자열 그대로 저장해서 비용은 많이 들지만
      • 나중에 Enum이 변경되어도 위험할일이 없음

(3) Setter 의 사용

참고: Entity에서 @Setter 사용을 지양하자

(4) 비교

① Entity vs Table

Entity

  • 데이터베이스나 SQL상 실제로 존재하는 것이 아닌, 일종의 개념
  • 논리 모델에서 사용
  • '실체', '객체'라는 의미
  • 업무에 필요하고 유용한 정보를 저장하고 관리하기 위한 집합적인 것

Table

  • 데이터베이스나 SQL에 실제로 존재하며 물리적인 구조를 지님

  • 물리 모델에서 사용

    name 속성의 사용

    @Entity(name = "...")
    - 엔티티의 이름을 정한다
    - @Table 없이 @Entity(name = "...")만 존재할 경우, 해당 @Entity의 name 속성에 의해 엔티티와 테이블의 이름이 모두 정해진다.

    @Table(name = "...")
    - DB 에 생성될 테이블의 이름을 정한다

② Entity vs Value

Entity

  • 엔티티 객체는 DB의 테이블과 대응될 수 있음
  • 식별자로 구분되기 때문에, 식별자를 제외한 데이터가 변경된다고 다른 객체가 되는 것은 아님

Value

  • 기본적으로 식별자가 없어 대응될 수 없다.
  • 식별자를 가지지 않으며, 값 그자체 --> 하나의 데이터라도 변경되면 그냥 다른 객체가 됨
    → 따라서, 같은 객체임을 확실하게 보장하기 위해 Value 타입 불변(immutable)으로 구현하는 것이 좋음
    → 방법1: 값은 생성자를 통해서만 받고, setter는 아예 구현하지 않음
    → 방법2: 기존 객체의 데이터를 변경하고 싶다면, 변경한 데이터로 아예 새로운 객체를 만든다.

3) DTO (Data Transfer Object)

(1) 정의

  • 계층(Layer) 간 데이터 교환을 하기 위해 사용하는 객체(Java Beans)
    → 계층 사이의 각각의 DTO 는 100% 무조건 분리돼있어야 한다.

  • 직렬화에도 사용되는 객체(JSON serialization 등...)

  • 로직(프로그램을 만들 때의 논리적인 흐름)을 가지지 않는 데이터 객체

  • getter/setter 메소드만 가진 클래스
    (이 외의 비즈니스 로직은 포함 X)

  • DB에서 꺼낸 데이터를 저장하는 Entity를 가지고 만드는 일종의 Wrapper(데이터 타입 변환을 도움)

  • Entity를 Controller 같은 클라이언트단과 직접 마주하는 계층에 직접 전달하는 대신 DTO를 사용해 데이터를 교환

  • Request와 Response용 DTO는 View를 위한 클래스

  • Dto를 Entity 로 변환하여 DB 에 저장한다
    → toEntity() 메서드를 통해, DTO 에서 필요한 부분을 이용하여 Entity 로 만든다.

(2) 사용 이유

  1. View Layer(객체를 표현하기 위한 계층, Controller Layer)와 DB Layer(저장하는 계층)의 역할을 분리하기 위해

  2. Entity 객체의 변경을 피하기 위해
    Entity 객체를 그대로 사용하면, 프로그래머의 의도와 다르게 데이터가 변질될 수 있다.

  3. Entity 의 변경을 최소화하기 위해
    View와 통신하는 DTO 클래스(예를 들어 ResponseDTO, RequestDTO)가 자주 변경된다.
    (어떤 요청에서는 특정 값이 추가될 수도 있고 특정값이 없을 수도 있다.)
    따라서, Entity 클래스와 분리하여 관리해야 한다.

  4. 도메인 모델링을 지키기 위해
    Entity 클래스의 특정 컬럼들을 조합하여 특정 포맷을 출력하고 싶다 때
    Entity 클래스에 표현을 위한 필드나 로직이 추가되면 객체 설계를 망가뜨릴 수 있으나,
    DTO에 표현을 위한 로직을 추가해서 사용하면 Entity의 도메인 모델링을 지킬 수 있다.

  5. private 필드에 접근하기 위해

    public class MemberDto {
        private int num;
        private String name; 
        private String addr;
    
        public void setNum(int num) {		// num 필드에 대한 setter 메소드
            this.num = num;
        }
    
        public void setName(String name) {		// name 필드에 대한 setter 메소드
            this.name = name;
        }
    
        public void setAddr(String addr) {		// addr 필드에 대한 setter 메소드
            this.addr = addr;
        }
    
        public int getNum() {		// num 필드에 대한 getter 메소드
            return num;
        }
        public String getName() {		// name 필드에 대한 getter 메소드
            return name;
        }
        public String getAddr() {		// addr 필드에 대한 getter 메소드
            return addr;
        }
    }
    public class MainClass12 {
        public static void main(String[] args) {
    
            // MemberDto 객체 생성해서 참조값을 dto1 에 담기
            MemberDto dto1 = new MemberDto();	
    
            // DTO를 활용하여, private 필드에서 setter 메소드로 num에 1 을 전달
            dto1.setNum(1);		
            dto1.setName("김구라");
            dto1.setAddr("노량진");
    
            // 원래는 private 필드에 접근이 불가하나, dto1 에 저장된 값을 참조하여 getter 메소드로 활용
            int num = dto1.getNum();
            String name = dto1.getName();
            String addr = dto1.getAddr();
        }
    }

(3) inner class, inner static class

① 정의

// inner class
public class User{
    class admin{
    }
}

// inner static class
public class User{
    static class admin{
    }
}

inner class(내부 클래스)는 말 그대로 중첩 클래스 中 내부에 선언된 클래스

② inner class 과 inner static class 비교

  1. 선언문

    // inner class : 내부 클래스로 인스턴스 2개 생성
    MyClass.InnerClass a = new MyClass().new InnerClass();
    MyClass.InnerClass b = new MyClass().new InnerClass();
    /* result: a != b (다른 참조) */
    
    // inner static class : 내부 정적 클래스로 2개 생성
    MyClass.InnerClass c = new MyClass.InnerStaticClass();
    MyClass.InnerClass d = new MyClass.InnerStaticClass();
    /* result: c != d (다른 참조) */
  2. 외부 참조

    // inner class : 외부 클래스의 인스턴스를 만들고, 다시 InnerClass 생성 시 참조 가능   
    MyClass a = new MyClass();
    MyClass.InnerClass b = a.new InnerClass();		// b 는 a 에 대한 숨은 외부 참조를 갖는다.
    
    // inner static class : innerStaticClass 는 해당 X
    MyClass.InnerStaticClass c = new MyClass.InnerStaticClass();	// c 는 해당 X
    class MyClass {
        void myMethod() {
            ...
        }
    
        // inner class 적용
        class InnerClass{
            void innerClassMethod() {
                MyClass.this.myMethod(); // 내부 코드에서도 외부클래스의 속성 참조에서 차이를 보인다. 숨은 외부 참조가 있기 때문에 가능
            }
        }
    
        // inner static class 적용
        static class InnerStaticClass{
            void innerClassMethod() {
                MyClass.this.myMethod(); // 컴파일 에러
            }
        }
    }

③ 어떤 것을 사용해야할까

inner Class
1. 참조값을 담아야 하므로, 인스턴스 생성 시 시간적/공간적으로 성능 ↓
2. 외부 인스턴스에 대한 참조가 존재하므로, 가비지 컬렉션이 인스턴스 수거를 하지 못하여 메모리 누수가 생길 수 있다.
3. 바깥 인스턴스와 암묵적으로 연결이 되어 있으므로, 바깥 인스턴스 없이는 멤버 클래스를 생성할 수 없다. (클래스명.this)

inner static class
중첩 클래스의 내부 클래스가 바깥 인스턴스와 독립적으로 존재할 수 있다면,
정적 멤버 클래스로 만들어서 바깥 인스턴스로의 외부 참조를 가지지 않게 만들면 된다.
→ 외부 참조를 저장할 메모리가 사라지므로, 메모리 누수X 메모리 사용↓ 생성 시간↓

결론
innerClass 를 생성해야할 경우, 외부 클래스로 빼도록 하고
그게 아니면, 내부 클래스 생성 시 static으로 사용하는게 적합하다.

④ DTO 에 적용

잘못된 사용

Member

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    public Member(final String name) {
        this.name = name;
    }
}

Dto

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MemberCreateRequest {
    private String name;
}

@Getter
@AllArgsConstructor
public class MemberCreateResponse {
    private Long id;
    private String name;
}

이 경우에 Member를 create 하는 기능이 필요할 시,
MemberCreateRequest, MemberCreateResponse 벌써 두 개의 DTO 클래스가 필요해진다.
→ 여러 DTO 가 계속 필요해지는 상황이 온다면, MemberxxxRequest 이런 DTO 클래스가 점점 늘어나게 된다.
→ DTO 를 선별/구분하는데 인적 리소스가 소모되게 될 것이다.

올바른 사용

Member

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    public Member(final String name) {
        this.name = name;
    }

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Request {
        private String name;
    }

    @Getter
    @AllArgsConstructor
    public static class Response {
        private Long id;
        private String name;
    }
}

inner class 를 이용해, Member 안에 Request와 Response DTO를 가지게 한다.
→ Member 클래스만 보고 관련된 DTO를 빠르게 찾을 수 있다.
→ 클래스 파일 개수↓ 인적 리소스↓ 개발의 편의성↑

Controller

@RestController
@RequestMapping("/api/member")
public class MemberController {

    @GetMapping("/{id}")
    public ResponseEntity<Member.Response> getMember(@PathVariable final Long id) {
        return ResponseEntity.ok(new Member.Response(id, "unluckyjung"));
    }

    @PostMapping
    public ResponseEntity<Member.Response> create(@RequestBody final Member.Request request) {
        //...저장 로직
        return ResponseEntity.created(URI.create(String.format("/api/member/%d", 1L)))
                .body(new Member.Response(1L, request.getName()));
    }
}

Controller 에서 inner class 를 요청/응답한다.

4) Entity 와 DTO 분리 이유

  1. Entity 와 DTO 의 역할 차이 (DB Layer 와 View Layer 의 역할을 철저하게 분리)

    Entity

    • JPA 이용 시, 단순히 데이터를 담는 객체가 아니라 실제 DB 와 관련된 중요한 역할
    • 내부적으로 EM(EntityManager)에 의해 관리되는 객체
    • Entity의 생명주기(Life Cycle)도 전혀 다르다 (DTO 와 비교해서)

    DTO

    • 각 계층끼리 주고받는 우편물이나 상자의 개념
    • '전달' 자체가 목적
    • 읽기/쓰기 가능
    • 일회성으로 데이터를 주고받는 용도로 사용
      → 일회성의 성격
  2. 정보가 불일치 할 경우

    테이블에 mapping되는 정보와 실제 View에서 요청되는 정보가 다를 경우
    테이블에 필요한 정보에 맞게 데이터를 변환하는 로직이 필요할 수 있는데, 이때 해당 로직이 Entity 에 들어가게 되면 지저분해진다.

  3. 정보 노출

    DB 로부터 조회된 Entity 를 그대로 View로 넘길 경우
    불필요한/노출되면 안 되는 정보까지 노출될 수 있고, 이를 막기 위한 로직을 따로 구현해야 한다.

3. VO (Value Object)

1) 정의

@Getter 
@Setter
@Alias("example")
class exampleVO {
    private Long a;
    private String b;
    private String c;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Example example = (Example) o;
        return Objects.equals(id, example.a);
    }

    @Override
    public int hashCode() {
        return Objects.hash(a);
    }
}
  • 값 자체를 표현하는 객체

  • 객체들의 주소 값이 달라도, 데이터 값이 같으면 동일한 것으로 여긴다.
    → 내용물이 값 자체를 의미하므로, 'read only'의 특징을 가진다.

  • 값 비교를 위해, equals()와 hashCode() 메서드를 오버라이딩
    → VO 내부에 선언된 속성(Field)의 모든 값들이 VO 객체마다 값이 같아야, 똑같은 객체라고 판별

2) DTO vs VO

DTO

  • 인스턴스 개념

  • 단지 데이터를 담는 그릇의 역할일 뿐 값은 그저 전달되어야 할 대상일 뿐
    → Layer 간의 통신을 위한 객체
    → 전달될 데이터를 보존해야함

VO

  • 리터럴 값 개념

  • 읽기만 가능한 read-only 속성을 가진 객체
    → 값들에 대해 Read-Only를 보장해줘야 존재의 신뢰성이 확보

  • 특정 비즈니스 값을 담는 객체
    → 값 자체에 의미가 있음


참고: [Java] DTO(Data Transfer Object) or VO(Value Object) /getter(), setter()
참고: 역할 분리를 위한 Entity, DTO 개념과 차이점
참고: Entity, DTO, VO 차이
참고: [DAO] DAO, DTO, Entity Class의 차이
참고: [베네픽처 홈페이지] Controller, Repository 분리
참고: DTO 관리 - Inner class
참고: [Java] inner class 와 inner static class 차이
참고: DTO를 inner class로 관리하기
참고: Entity
참고: @Table과 @Entity 차이점
참고: @GeneratedValue 전략

profile
개발자로 거듭나기!

0개의 댓글