본문 바로가기
SPRING/자바,스프링 개발자를 위한 실용주의 프로그래밍

[스프링] 객체의 종류 예제 실습과 함께 알아보기

by 잉나영 2024. 7. 12.
반응형

자바를 다루는 개발자를 보면 스프링에 대한 다양한 객체를 접하게 된다.

하지만 개발자마다 의미 정의가 다를 수도 있고, 용어에 대한 확실한 정리가 되지 않았을 수도 있다. 

그래서 이번 차시에는 스프링을 다루는 객체의 종류를 알아보고, <자바/스프링 개발자를 위한 실용주의 프로그래밍> 책을 활용하여  실습을 진행해 볼 것이다. 

 

순서는 아래와 같다.

  1. VO (Value Object) : 값 개체
  2. DTO (Data Transfer Object) : 데이터 전송 객체
  3. DAO (Data Access Object) : 데이터 접근 객체
  4. 엔티티 (Entity) : 개체

 

1. VO (Value Object)  : 값 개체

 

예제와 함께 알아봅시다.

 

package HW.Spring.VO;

import java.util.Objects;

public final class Color {
    public final int r;
    public final int g;
    public final int b;
    
    public Color(int r, int g, int b){
        if(r < 0 || r > 255||
        g < 0 || g > 255 ||
        b < 0 || b > 255 ){
            throw new IllegalArgumentException("RGB should be 0 to 255");
        }
        this.r = r;
        this.g = g;
        this.b = b;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final Color color = (Color) o;
        return r == color.r &&
                g == color.g &&
                b == color.b;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(r,g,b);
    }
}

 

 


 

코드를 해석해 보면 세 가지로 분리가 된다.

public Color(int r, int g, int b){
    if(r < 0 || r > 255 ||
    g < 0 || g > 255 ||
    b < 0 || b > 255 ){
        throw new IllegalArgumentException("RGB should be 0 to 255");
    }
    this.r = r;
    this.g = g;
    this.b = b;
}

 

생성자로서 r, g, b를 매개변수로 받아들이면서 0~255 사이의 값인지 확인한다.

 

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
    final Color color = (Color) o;
    return r == color.r &&
            g == color.g &&
            b == color.b;
}

 

객체의 동등성을 비교하기 위해 equals 메서드를 재정의한다.

현재 객체와 비교 객체의 RGB 값이 모두 동일하면 true를 반환하고, 그렇지 않으면 false를 반환한다.

 

@Override
public int hashCode() {
    return Objects.hash(r, g, b);
}

 

RGB 값을 기반으로 해시 코드를 생성한다.

 

이 클래스는 주어진 RGB 값이 유효한지 확인하며, equals와 hashCode를 재정의하여 Color 객체의 비교와 해시 기반 컬렉션에서의 사용을 지원합니다. final 필드를 사용하여 객체가 생성된 이후에는 수정할 수 없게 하여 불변성을 유지합니다.


 

하지만 우리의 알고자 하는 것은 값 객체인가에 대한 질문이다.

답을 하면 

Color 클래스는 값 객체가 된다.

즉, Color는 객체지만 동시에 값이다. 

 

그러면 값의 특징은 무엇인가?

 

값의 특징 설명
불변성 값은 변하지 않는다. 
ex) 숫자 2는 영원한 숫자 2이다.
동등성 값의 가치는 항상 같다.
ex) 모든 숫자 2는 적혀있는 위치나 시간과 관계없이 항상 같은 숫자 2이다.
자가검증 값은 그 자체로 올바르다.
ex) 숫자 2는 그 자체로 올바른 숫자이다. 2는 사실 2.002이지 않을까와 같은 고민이 필요없다.

 

 


(1) 불변성 : 변하지 않는다.

 

이 개념이 시스템의 복잡도를 획기적으로 낮출 수 있고, 소프트웨어 중 일부를 예측하며 신뢰할 수 있게 만들어준다.

 

소프트웨어에는 불확실한 요소가 너무 많아서 확실한 영역을 최대한 많이 만들어내는 것이 중요하다.

여기서 믿을 수 있는 코드란 항상 변하지 않고(불변성) 똑같은 결과와 똑같은 값만 돌려주는 코드를 의미한다.

 

불변성이라는 개념을 자바에서 어떻게 구현할 수 있는가?

 final을 사용

 

위 코드에서 r, g, b는 final로 선언이 되었다. 즉 한 번 생성된 Color 인스턴스의 r, g, b값은 변하지 않은 것이다. 

따라서 VO는 불변성이라는 특징을 갖고 있는 객체를 말하므로, 모든 멤버 변수는 불변(final)으로 선언되어야 한다!

 

 

하지만 모든 멤버 변수가 final로 선언되어 있다 해서 VO는 아니다. final로 선언을 하더라도 원시타입이 아닌 참조 타입인 객체가 있다면 불변성이 보장되지 않을 수 있다. 

아래 코드와 함께 이해해 보자.

 

@Getter
public class FilledColor {
    
    public final int r;
    public final int g;
    public final int b;
    public final Shape shape; // FilledColor은 지정된 Shape에 들어가는 색상 의미
    
    public FilledColor(int r, int g, int b, Shape shape) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.shape = shape;
    }
    
    @Data
    class Retangle extends Shape {
        private int width; // 하지만 Shape에 들어갈 수 있는 Rectangle 클래스의 멤버변수 불변이 아님
        private int height;
        
        public Rectangle(int weight, int height){
            this.width = width;
            this.height = height;
        }
    }
}

 

이와 같이 FilledColor 클래스의 모든 멤버 변수가 final로 선언이 되었지만, 참조 객체 Retangle의 width, height가 불변성이 지켜지지 않아서

이 코드는 불변성이 깨진다.

 

또한, 불변성은 변수에만 적용되는 개념이 아니다.

불변성은 함수에도 적용될 수 있는데, 항상 같은 값을 반환하는 함수를 순수함수라고 부른다. 

멤버 변수와 마찬가지로 VO 안의 모든 함수는 순수함수여야 한다.

 

따라서 우리는 100점짜리 VO를 만드는 것이 아닌 불변성이 지닌 가치를 쫓아야 한다.

불변성은 객체를 신뢰할 수 있게 만들기 위함이다. 

불변성이 가진 객체는 내부 상태가 변경되지 않고, 협력과정 중 항상 예측 가능한 방식으로 동작된다. 

 

협력하는 과정에서 예측할 수 없게 동작한다는 것은 무슨 의미인가?

아래 코드와 함께 설명해 보겠다.

 

public class AccountInfo {
    
    private long mileage; 
    
    public AccountLevel getLevel() {
        if(mileage > 100_000) return AccountLevel.DIAMOND;
        else if (mileage > 50_000) return AccountLevel.GOLD;
        else if (mileage > 30_000) return AccountLevel.SILVER;
        else if (mileage > 10_000) return AccountLevel.BRONZE;
        else return AccountLevel.NONE;
    }
    
    public void setMileage(long mileage) {
        this.mileage = mileage;
    }
}

 

위의 코드는 mileage변수에 따라 AccountLevel의 열거형을 반환한다. 

만약 이 객체를 참조하는 스레드가 여러 개일 경우 

 

스레드 A와 B가 협업하는 상황

 

스레드 B에서 마일리지를 변경하면 스레드 A입장에서는 객체가 불확실해진다.

-> 불변성이 어긋남

 

 

AccountInfo를 불변으로 만들어보면

@AllArgsConstructor
public class AccountInfo {

    public final long mileage; // final로 선언하고 @AllArgsConstructor 애너테이션 지정

    public AccountLevel getLevel() {
        if(mileage > 100_000) return AccountLevel.DIAMOND;
        else if (mileage > 50_000) return AccountLevel.GOLD;
        else if (mileage > 30_000) return AccountLevel.SILVER;
        else if (mileage > 10_000) return AccountLevel.BRONZE;
        else return AccountLevel.NONE;
    }

// 세터가 사라진 대신 변경 요청이 들어올 때 새로운 객체를 반환하는 메서드 추가
   public AccountInfo withMileage(long mileage) {
        return new AccountInfo(this.id, mileage);
   }
}

 

불변성이 갖춰진 멀티 스레드 환경

 

어긋나지 않게 멀티 스레드 환경을 가질 수 있다.


(2) 동등성

 

동등성을 추구해야 하는 이유부터 예제와 함께 알아보자.

public class Color {
    
    public final int r;
    public final int g;
    public final int b;
    
    public Color(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }
}

 

위와 같은 코드에서 아래와 같은 질문을 했을 때의 답변은 어떻게 나올까?

Color green1 = new Color(0, 1, 0);
Color green2 = new Color(0, 1, 0);
System.out.println(green1 == green2);

 

A1 : 의미론적으로 초록색이라는 색상은 같아 true이다.

A2 : 다른 참조 값을 갖는 개체이니 false다.

 

→  green1 == green2의 예측이 불가해지면서 불확실성 발생

 

VO의 입장에서는 불확실성 문제에 어떻게 생각하는가?

 

어떤 객체가 값이고 상태가 모두 같다면 같은 객체로 보아야 한다.
@Override 통해 상태를 비교하는 코드로 변경해 준다.

 

 

※ VO에는 식별자를 넣어서는 안 된다. 즉, id 같은 식별자 필드를 멤버변수로 가지면 예측 불가능성이 다시 발생한다.

 


(3) 자가 검증

 

: 클래스 스스로 상태가 유효한지 검증할 수 있음을 의미한다.

즉, 유효하지 않은 상태의 객체가 만들어질 수 없다는 것!

 

아까 Color코드에서 자가검증이 들어가면서 좀 더 정확성을 높여준다.

public Color(int r, int g, int b){
        if(r < 0 || r > 255||
        g < 0 || g > 255 ||
        b < 0 || b > 255 ){
            throw new IllegalArgumentException("RGB should be 0 to 255");
        }
        this.r = r;
        this.g = g;
        this.b = b;
    }

 

자가검증을 통하여 유효한 값이 들어있는지에 대해 확인해 볼 수 있다. 

 

결론 : VO를 추구하기보다 불변성, 동등성, 자가 검증, 신뢰할 수 있는 객체를 추구해야 한다.

 


2. DTO (Data Transfer Object) : 데이터 전송 객체

 

public class UserCreateRequest {
    
    public String username;
    public String password;
    public String email;
    public String address;
    public String gender;
    public int age;
}

 

메서드를 호출할 때 매개변수로 사용이 가능하다.

userService.create(userCreateRequest);

 

이러한 DTO 클래스를 만드는 이유

: 다른 객체의 메서드를 호출하거나 시스템을 호출할 때 매개변수를 일일이 모두 나열하지 않아도 되기 때문이다.

 

DTO는 다른 객체나 시스템에 데이터를 구조적으로 만들어 전달하기 위한 객체
그저 데이터를 하나하나 일일이 나열해서 전달하는 게 불편해서 데이터를 하나로 묶어서 보내려고 만들어진 객체

 

 

하지만 아래와 같이 DTO에 대한 오해들이 발생한다.

  1. DTO는 프로세스, 계층 간 데이터 이동에 사용된다.
  2. DTO는 게터, 세터를 갖고 있다.
  3. DTO는 데이터베이스에 데이터를 저장하기 위해 사용되는 객체다.

(1) DTO는 프로세스, 계층 간 데이터 이동에 사용된다.

 

DTO는 API 통신이나 데이터 베이스 통신에 사용할 수 있으나 목적은 아니다.

-> DTO의 주요한 목적은 데이터를 전달하는 것.

 

(2) DTO는 게터, 세터를 갖고 있다.

 

게터, 세터는 내부 데이터를 전달하기 위한 방법 중 하나이지 없이도 내부 데이터 전달이 가능하다.

-> public으로 멤버변수를 선언 가능.

 

(3) DTO는 데이터베이스에 데이터를 저장하는 데 사용되는 객체다. 

 

-> 그렇지 않다. 데이터를  전송하기 위한 객체이지 그 이상 이하도 아니다.

 


 

3. DAO (Data Access Object) : 데이터 접근 객체

 

[DAO 역할]

  • 데이터베이스와의 연결을 관리
  • 데이터베이스에 연결해 데이터에 대한 CRUD 연산 수행
  • 보안 취약성을 고려한 쿼리 작성

DAO는 데이터에 접근하기 위한 객체로서 친숙한 리포지터리와 같은 개념이다.

 

DAO의 목적 : 도메인 로직과 데이터베이스 연결 로직을 분리하기 위해서

 


4. 엔티티 (Entitiy) : 개체

 

  • 도메인 엔티티
  • DB 엔티티
  • JPA 엔티티

(1) 도메인 엔티티

 

도메인 : 비즈니스 영역

도메인 모델 : 도메인 문제를 해결하고자 만들어진 클래스 모델

도메인 엔티티 : 도메인 모델 중에서 특별한 기능을 갖고 있는 모델들

 

 

[특징]

  • 식별 가능한 식별자를 갖는다.
  • 비즈니스 로직을 갖는다.

도메인 엔티티 : 식별 가능하고 비즈니스 로직을 갖고 있으며, 조금 특별하게 관리되는 클래스로 만들어진 객체 

 

 

일반적으로 소프트웨어 개발 분야에서 엔티티는 도메인 엔티티를 뜻한다.

도메인 엔티티는 도메인 모델에 포함되는 개념

 

 


(2) DB 엔티티

: 유무형의 객체를 표현하기 위한 수단

: 도메인 엔티티의 개념과 상관없이 원래 관계형 데이터베이스 분야에서 어떤 유무형의 객체를 표현하는 데 사용한 용어

 

 


(3) JPA 엔티티

: 관계형 데이터베이스에 있는 데이터를 객체로 매핑하는 데 사용되는 클래스 @Entitiy로 지정

@Data
@NoArgsConstructor
@Entity(name = "user")
@Table(name = "user")
public class UserJpaEntity {
    
    @Id
    private String id;
    @Column
    private String name;
    @Column
    private String email;
}

 

JPA엔티티는 DB 엔티티의 뿌리를 두고 있다.

둘을 구분해야 하며 JPA엔티티로 인식하는 개발자가 되어 관계형 데이터베이스에 종속되는 프로그램을 제작해야 한다. 

 


(4) 엔티티 결론

 

  • 프로그래밍 언어와 데이터 분야에서의 유무형 자산 정보를 개체라고 부른다. 
  • 엔티티를 표현하는 데는 '클래스' 사용해서 표현
  • 데이터베이스 진영에서는 '테이블' 사용해서 표현

 

객체지향과 데이터베이스 분야 모두에서 유무형의 자산 정보를 엔티티라고 부름

 

둘을 다르게 선택하는 이유는 추구하는 바가 다르기 때문이다.

 

객체지향 : 온전한 객체와 객체들과의 협력을 추구

데이터베이스 : 데이터의 정합성, 중복 제거 추구

 

둘을 활용한 서비스를 제작할 때 1:1 매핑에 무리가 발생하였다.

 

객체지향 분야와 데이터 베이스 분야에서 엔티티를 표현하는 방식은 다르다.

 

개발자는 데이터베이스에서 데이터를 가져와 도메인 모델에다 데이터를 옮기는 작업을 실행하게 되었다.

MyBatis 라이브러리를 활용하여 DB에 쿼리를 던지고, 필요한 데이터를 읽어오는 데 성공하였다.

 

정보 매핑이 지루해질 때 쯤 등장한 ORM

 

더 발전해나가 ORM(object-relational mapping : 객체-관계 매핑) 솔루션 등장하였다.

ORM 솔루션 중 자바 진영의 대표적 솔루션 = JPA , 하이버네이트

 

[JPA 엔티티 요약]

1. JPA의 엔티티는 관계형 데이터베이스의 엔티티를 지칭하는 것이다.
2. JPA의 @Entity 애너테이션이 적용된 객체는 '영속성 객체'이다.
3. JPA의 @Entity 애너테이션은 영속성 객체를 만들기 위한 도구일 뿐이다.

 

 


 

엔티티는 역할과 책임에 의해 '결정되는 것'

[엔티티 정리본]

- 엔티티는 데이터로 표현하려는 유무형의 대상이다.

- DB 엔티티는 데이터베이스 분야에서 데이터로 표현하려는 대상이다.

- 소프트웨어 개발 분야에서 말하는 엔티티는 도메인 엔티티다.

- 도메인 엔티티는 도메인 모델 중에서도 식별 가능하고 비즈니스 로직을 갖고 있으며, 조금 특별하게 관리되는 객체다.

- JPA의 엔티티는 관계형 데이터베이스의 엔티티를 지칭하는 것이다.

- JPA 의 @Entity 애너테이션이 적용된 객체는 '영속성 객체'이다.

- JPA의 @Entity 애너테이션은 영속성 객체를 만들기 위한 도구일 뿐이다.

- 도메인 엔티티와 DB 엔티티는 다르다.

 


 

스프링에 대해 더 공부해보고 싶다면? 

 

구매링크 참조

https://product.kyobobook.co.kr/detail/S000213447953

 

반응형