본문 바로가기
DDD (도메인 주도 설계)

[DDD] 간접 참조 (feat. JPA)

by doyoungKim 2021. 7. 11.

간접 참조

 

 

도메인 주도 설계를 공부하다 보면 아래의 예시 이미지 처럼 도메인을 기준으로에그리거트 단위로 그룹핑을 하게 된다. 

애그리거트는 관련 도메인을 하나의 군집으로 묶은 것

회원 에그리거트 와 빵집 에그리거트

 

위의 이미지는 회원과 빵집의 관계 (회원이 빵집을 등록하는 관계) 를 나타낸 것 이다.

이 글에서는 회원 에그리거트와 빵집 에거리거트의 관계. 즉, "에그리거트와 다른 에그리거트의 관계" 에 대한 내용을 적어보겠다.

 

DDD 에서는 에그리거트간에 참조 방식은 직접 참조 방식 보다 간접 참조하는 방식을 권장한다.

 

용어 사전

Bakery: 빵집 에그리거트 
User: 회원 에그리거트

각각의 두 참조 방식을 예시를 들어서 설명 해보겠다.

1. 직접 참조 

JPA 에서 직접 참조는 Entity 클래스를 설계할 때 @OneToOne, @OneToMany  ... 와 같은 어노테이션을 써서 Entity 간에 연관 매핑하는 것이다.

 

다음은, 빵집이 회원을 직접 참조하는 엔티티 클래스 이다.

@Entity
@Getter
@NoArgsConstructor
@ToString
public class Bakery {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "bakery_id")
    private Long id;

    // 빵집 명
    private String title;

    // 빵집 주소
    @Enumerated
    private Address address;

    // 최초 등록자
    @OneToOne
    private User user; 
    
    @Builder
    public Bakery(Long id, String title, Address address, User user) {
        this.id = id;
        this.title = title;
        this.address = address;
        this.user = user;
    }
}

 

Bakery class 안에는 user 라는 변수가 있다. 현재 User 가 어떤 엔티티 인지는 자세하게는 모르지만,
위와 같은 에그리거트 간에 직접 참조의 방식은 빵집 에그리거트를 다룰 떄, 회원 에그리거트를 변경할 수 있는 가능성이 있고, 이는 실수를 의미한다.

먼저 "빵집 에그리거트를 다룰 때 회원 에그리거트를 변경해서는 안되는 건가?" 라는 의문이 생길 수도 있다.

 

그렇다면 회원 에그리거트의 상태가 변경되는 곳이 회원 에그리거트 외부에서 일어나는 것일까?


 

아래의 코드는, User 라는 Entity 날 것으로 가져와서 Bakery Entity 를 생성해 저장하는 코드이다.

    @Transactional
    public Long save(requestDto dto) {
    
        final String address = dto.getAddress();		
        final String title = dto.getTitle();
        final User user = userRepository.findById(dto.userid);
        
        final Bakery bakery = Bakery.builder()
                .title(title)
                .address(address)
                .user(user)
                .build();

        return bakeryRepository.save(bakery).getId();
    }

 

즉 User 라는 Entity 자체가 날것으로 가져오게 되면, Entity 가 오염이 될 수 도 있다. setter 를 통해서든, 도메인 서비스를 통해서든 어떤 일이 벌어질 수 있는 가능성을 열어둔 것이다.

약간 예시가 과하긴 했지만, 아래 코드 처럼 save 메소드 안에서 더티체킹으로 인해 유저가 갑자기 관리자가 되고, 닉네임이 끝빵왕이 될 수도 있다. 

    @Transactional
    public Long save(requestDto dto) {
    
    ...
        
        final User user = userRepository.findById(dto.userid);
        
        user.setRole(ROLE.ADMIN); // 관리자로 변경 !
        user.updateNickName("끝빵왕"); // 닉네임이 변경 !
        
        final Bakery bakery = Bakery.builder()
                .title(title)
                .address(address)
                .user(user)
                .build();

        return bakeryRepository.save(bakery).getId();
    }

 

따라서 User 에그리거트도 안전하게 보호가 되고, Bakery 에그리거트에만 집중할 수 있는 방법이 필요하고 그 방법이 바로 간접 참조를 이용하는 것이다.

결론 부터 이야기 해보자면, 

다른 에그리거트와 관계를 맺거나 이용하기 위해선 ResponseDto 를 반환하는 application layer 에 있는 서비스 객체를 통해야 하며, 날것의 Entity 클래스 그대로가 아닌 ! ResponseDto 에 있는 다른 루트 에그리거트의 아이디를 참조하는 방식간접 참조 방식으로 관계를 설정해야 한다.

 

2. 간접 참조 

간접 참조는 외래키를 맺지 않고 다른 루트 에그리거트의 식별값을 참조하는 방식이다.

 

다음은, 빵집이 회원을 간접 참조하는 엔티티 클래스 이다.

@Entity
@Getter
@NoArgsConstructor
@ToString
public class Bakery {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "bakery_id")
    private Long id;

    // 빵집 명
    private String title;

    // 빵집 주소
    @Enumerated
    private Address address;

    // 루트 에그리거트의 아이디를 참조하는 방식
    // 최초 등록자
    private Long user;

    @Builder
    public Bakery(Long id, String title, Address address, UserId user) {
        this.id = id;
        this.title = title;
        this.address = address;
        this.user = user.getValue();
    }
}
@Embeddable
@Getter
@NoArgsConstructor
public class UserId {
    
    private long value;

    public UserId(long value) {
        this.value = value;
    }
}
    @Transactional
    public Long save(requestDto dto) {
    
        final String address = dto.getAddress();		
        final String title = dto.getTitle();
        // application layer 에 있는 서비스 객체를 이용
        final UserResponseDto user = userService.findById(dto.userid);
        
        final Bakery bakery = Bakery.builder()
                .title(title)
                .address(address)
                .user(user.getUserId())
                .build();

        return bakeryRepository.save(bakery).getId();
    }

 

기존에 있었던 @OneToOne 어노테이션과 User 타입을 지우고, Long 타입으로 변경함으로써 User의 식별값만 받도록 수정했다.
또한 userid 를 Long 타입으로 매개변수를 받지 않고 UserId 를 매개변수로 받음으로써, 다른 이상한 Long 값을 대입하는 실수를 방지 할 수 있었다.

이렇게 간접 참조로 에그리거트 간에 관계를 맺으면 User 에그리거트도 안전하게 보호가 되고, Bakery 에그리거트에만 집중할 수 있다.

하지만 간접참조에 대한 문제점이 있는데, 그것은 N+1 Query 이다.
만약에 Bakery 리스트를 뿌려주는데 거기에 User 정보도 같이 보여줘야 한다면, Bakery 마다 User 정보를 호출하는 Query 를 사용하게 된다.

이와 같은 문제점은 CQRS 패턴을 사용하여 명령, 조회 를 나누어서 관리하도록 하자.

 

 

 

 

 

 

출처

 

DDD 도메인 모델의 군집, 애그리거트(Aggregate)

최범균 님의 DDD Start를 읽고 정리한 내용입니다. DDD 애그리거트(Aggregate) 테이블이 100개 이상 있는 ERD를 보고 있다고 생각해보자. 하나 하나 따라가보면 개별 테이블의 연관 관계는 알 수는 있지

private-space.tistory.com

 

 

DDD에서는 왜 간접 참조를 더 권장할까?

spring-pet-clinic-data-jdbc를 살펴보다 객체간의 연관 관계가 전부 간접 참조로 되어있어서 왜 그런지 알아보았다. DDD를 제대로 이해하지는 못했지만 DDD에서는 간접 참조를 권장한다. 그 이유에 대해

dundung.tistory.com

 

 

애그리거트

주문은 상품, 회원, 결제와 관련이 있다는 것을 쉽게 파악할 수 있다. 위 그림처럼 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다. ( 주요 도메인 개념 간의 관계를

velog.io

 

728x90

댓글