컴포지션

김동헌·2023년 11월 24일
0

SpringBoot

목록 보기
10/19
post-thumbnail

프로젝트를 진행 중에 다양한 테마(여행 코스, 음식점, 관광지 등)을 선택하면 해당 위치 정보를 제공해야하는 일이 생겼습니다.

각각의 테이블에 대한 매핑을 위해서는 별도의 엔티티 클래스 파일을 생성해야 합니다.

동일한 내용의 엔티티를 여러 테이블에 매핑하려면 엔티티 클래스를 복사하여 새로운 파일을 만들고, 해당 파일에서 @Table 어노테이션을 사용하여 새로운 테이블 이름을 지정해주어야 합니다.

예를 들어, 기존의 Travel 엔티티 클래스를 사용하여 또 다른 테이블에 매핑하려면 같은 코드를 내포한 엔티티 클래스 2개를 생성해야 하는데 너무 비효율적이지 않나요 ?
아래 코드를 먼저 살펴보죠.


import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity // 엔티티로 지정
@Getter // Hetter 메서드를 대체
// 접근제어자를 명시적으로 선언, default public
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "travel_table")
public class Travel {
    /*
    @Id : JPA에서 엔티티 클래스의 기본 키를 나타내는 클래스로
            JPA는 자바 객체와 데이터베이스 간의 매핑을 지원,
            이때, 엔티티 클래스의 특정 필드를 기본 키로 사용하고자 할 때 사용

    updatable = true (기본 값) : 해당 필드는 업데이트가 가능, 즉시 데이터베이스에 변경된 값을 반영할 수 있음
    updateble = false : 해당 필드는 업데이트 불가능, 데이터베이스에 영향을 주지 않음.
                    주로 읽기 전용(read-only)필드나, 특정 조건에서만 업데이트가 가능한 필드 등에 사용
     */

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sequence", updatable = false)
    private long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "latitude", nullable = false)
    private double latitude;

    @Column(name = "longitude", nullable = false)
    private double longitude;

    @Column(name = "address", nullable = false)
    private String address;

    @Builder
    public Travel(String title, double latitude, double longitude, String address, int post_code, String out_line, String detail_info) {
        this.title = title;
        this.latitude = latitude;
        this.longitude = longitude;
        this.address = address;
    }

}

이 코드에서 같은 내용의 leports 테이블의 컬럼을 조회하려고 한다면 모든 속성(title, latitude, longitude, address)은 중첩되게 됩니다. 위치정보를 제공할 것이기 때문이죠.

그렇다면 효율적인 코드를 작성하기 위해 어떤 방법을 사용할 수있을까요 ?


상속 활용과 컴포지션

상속 활용(@MappedSuperclass)

상속 활용 참고 자료

동일한 코드를 갖는 두 개의 엔터티 클래스에서 공통된 속성과 메서드를 추상 클래스나 인터페이스로 분리하여 상속을 활용할 수 있습니다. 예를 들어, 공통된 필드와 메서드를 갖는 추상 클래스를 만들고, 각각의 테이블에 대한 엔터티 클래스가 이 추상 클래스를 상속받도록 구현할 수 있습니다.

상속 활용
장점코드 재사용성, 계층 구조
단점강한 결합,다중 상속의 한계, 상속 오용

컴포지션(@Embeddable)

컴포지션(Composition)은 객체 지향 프로그래밍에서 하나의 클래스가 다른 클래스의 객체를 포함하는 것을 말합니다. 이것은 관계형 데이터베이스에서 테이블 간의 연관성을 나타내는 것과 유사한 개념입니다.

컴포지션은 두 클래스 간의 "포함 관계"를 나타내며, 자식 클래스가 부모 클래스의 객체를 필드로 가지는 것을 의미합니다. 이를 통해 코드 재사용성을 높이고, 클래스 간의 결합도를 낮출 수 있습니다.

컴포지션
장점유연성, 코드 재사용성, 인터페이스를 통한 확장
단점조금 더 많은 코드, 다소 복잡성

그렇다면 어떤 것을 선택 ?

모든 코드에 정답이 없듯이 모든 방법들은 상황에 따라 다릅니다. 프로젝트의 특정 요구사항과 구조에 따라 상속 또는 컴포지션을 선택하게 됩니다.

유지보수성 : 변경이 예상되는 상황에는 컴포지션을 고려합니다.

객체지향 설계 원칙 : SOLID 원칙을 고려하여 코드의 단일 책임, 개방/폐쇄 원칙 등을 준수하는 방법을 선택합니다.

일반적으로는 컴포지션이 강력한 유연성을 제공하므로, 특별한 이유가 없다면 컴포지션을 사용하는 것이 좋습니다. 그러나 각 상황에 맞게 상황을 고려하여 선택하는 것이 중요합니다.


컴포지션을 사용해보자 !

저의 경우는 위치정보를 객체로 묶어서 관리하려고 합니다.
이때, 사용하는 어노테이션은 @Embedded, @Embeddable이 있습니다.

@Embedded@Embeddable은 JPA(Java Persistence API)에서 관계형 데이터베이스의 테이블 간에 객체를 포함하는(embedding) 방법을 나타내는 어노테이션입니다. 이들 어노테이션은 객체 간의 관계를 매핑하고 복합(Composite) 객체를 다루는 데 사용됩니다.

@Embedded

@Embedded 어노테이션은 복합 객체를 포함하는 엔티티 클래스의 필드에 부여됩니다.
이 어노테이션을 사용하면 엔티티 클래스에 다른 클래스의 객체를 포함시킬 수 있습니다.
포함된 객체의 필드들은 엔티티 테이블에 함께 매핑됩니다.

@Embeddable

@Embeddable 어노테이션은 복합 객체(Composite Object)를 나타내는 클래스에 부여합니다. 여기서 복합 객체란 여러 개의 속성을 포함하고 있는 객체로, 특별한 식별자 없이 자체적으로 의미가 있는 객체입니다.
이 어노테이션을 클래스에 부여하면 해당 클래스의 객체가 다른 엔티티에 포함될 수 있다는 것을 의미합니다.


코드

먼저 공통적인 정보를 @Embeddable을 사용해서 정의합니다.
아래 코드에서는 private TravelCommonData travelCommonData가 됩니다 !

Travel.java

@Entity // 엔티티로 지정
@Getter
// 접근제어자를 명시적으로 선언, default public
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "travel_table")
public class Travel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sequence", updatable = false)
    private long id;

    @Embedded
    private TravelCommonData travelCommonData;

}

@Embeddable 어노테이션을 이용해서 공통되는 부분들을 묶어줍니다 !

  • 이제알았는데 제목은 공통되지 않네요,, 수정해야겠습니다 : >

TravelCommentData

@Embeddable
@Getter
public class TravelCommonData {
    private String title;
    private double latitude;
    private double longitude;
    private String address;
}

아래는 Response 관련 코드인데 get 사용 부분을 확인해주시면 되겠네요 !

TravelResponse.java

@Getter
public class TravelResponse {
    private final String title;
    private final double latitude;
    private final double longitude;
    private final String address;
    private final int post_code;
    private final String out_line;
    private final String detail_info;

    public TravelResponse(Travel travel) {
        this.title = travel.getTravelCommonData().getTitle();
        this.latitude = travel.getTravelCommonData().getLatitude();
        this.longitude = travel.getTravelCommonData().getLongitude();
        this.address = travel.getTravelCommonData().getAddress();
    }
}

저는 @Getter 어노테이션을 사용했는데 또 this.title = travel.getTravelCommonData().getTitle(); 이렇게 하는 방식이 이해가 안되어서 알아보았습니다.

@Getter 어노테이션은 롬복(Lombok) 라이브러리에서 제공하는 어노테이션으로, 해당 클래스의 필드에 대한 게터(Getter) 메서드를 자동으로 생성해줍니다. 따라서 TravelCommonData 클래스@Getter 어노테이션이 적용되어 있다면 해당 클래스의 인스턴스 변수들에 대한 게터 메서드를 직접 작성하지 않아도 됩니다.

그러나 TravelResponse 클래스에서는 TravelCommonData의 인스턴스를 통해 데이터를 가져와서 사용하고 있습니다. 롬복은 TravelResponse 클래스에는 @Getter를 적용하지 않았기 때문에 해당 클래스에 대한 게터 메서드는 생성되지 않습니다.

따라서 TravelResponse 클래스에서도 getTravelCommonData() 메서드를 사용하여 TravelCommonData의 인스턴스를 얻고, 그 후에 해당 인스턴스의 필드에 접근해 값을 가져오는 방식을 사용하고 있습니다.
롬복을 사용하더라도 TravelResponse 클래스에 필요한 게터 메서드를 직접 작성하거나 롬복의 @Getter 어노테이션을 적용하여 해당 클래스의 게터 메서드를 자동으로 생성할 수 있습니다.

또한 기존의 코드를 아래처럼 작성하니 가독성이 더 좋아졌습니다 !

    public TravelResponse(Travel travel) {
        TravelCommonData commonData = travel.getTravelCommonData();
        this.title = commonData.getTitle();
        this.latitude = commonData.getLatitude();
        this.longitude = commonData.getLongitude();
        this.address = commonData.getAddress();
    }
profile
백엔드 기록 공간😁

0개의 댓글