객체지향-연관관계

ttomy·2022년 7월 30일
0

intro

https://www.youtube.com/watch?v=dJ5C4qRqAgA
조영호님의 테크세미나 영상을 보고 객체지향적설계에 대해 생각해 보고 나름의 리팩토링을 해보려 한다.

의존성 사이클의 제거

패키지끼리의 의존성의 사이클이 있을 때 이를 해결하는 방법이 있다.

  • 중간 객체를 두기
  • 객체참조(연관관계)를 repository의 id참조로 대체

중간 객체를 두기


클래스를 한층 더 추상화시켜서 의존성이 한 방향으로 흐르게 되었다.

처음에 이 구조도를 보고는
'그러면 order도메인의 service에서 shop객체에 접근해서 optionGroup과 Option을 사용하는 건가?' 생각했다. 그러면 orderservice의 코드가 좀 깔끔하진 못할 수 있겠다고 보았다.

하지만 더 공부해보니 다른 도메인에 의존하는 부분을 또 하나의 클래스로 만들어 응집력을 높일 수가 있다.
내가 받아들인 느낌으로 간략하게 말해보자면 다른 도메인까지 얽혀있는 부분을 떼어내 따로 만듦으로써 여러 도메인들이 참여되는 로직을 더 명확히 확인하게 만들 수 있다는 것이다. 아래의 방법과도 관련이 있으니 살펴보자.

객체참조를 객체의id참조로 대체

패키지의 의존성을 관리하는 방법이기도 하지만, 이는 곧 다른 도메인의 클래스끼리의 양방향 연관관계를 정리하는 방법도 될 수 있어보인다.

양방향 연관관계가 권장되지 않는다는 것은 알고 있었다.
하지만 양방향 연관관계를 대체할 방법을 알지 못해서 서로 조회가 필요한 경우가 있다면 '어쩔 수 없지...'하며 그냥 양방향을 사용해버렸었다.

하지만 도메인의 repository를 이용하면 연관관계 없이도 조회가 가능하기에 객체로딩을 어디까지 해야하는지의 문제에서 더 자유로워질 수 있어보인다.
아래의 두 그림을 보자.

객체를 직접 참조하지 않고 id만 참조한 후, 타 도메인 객체가 필요하다면 repository를 이용해 가져올 수 있다. 이로써 eager,lazy로딩만을 통해 객체로딩을 하는 것보다 명확하게 필요한만큼의 데이터를 당겨올 수가 있다.

  • 객체 연관관계의 설계 시 고려할 기준

    • 가능하면 분리
    • 라이프사이클의 유사성
    • 도메인 제약사항의 공유여부
    • 의존성은 강함(서로의 수정에 따라 변경이 빈번한가)

    shop과 order처럼 분리될 만한 도메인은 객체참조가 아닌 ID와 repository를 통해 객체참조을 없애고, 객체그룹의 조회경계를 보다 명확히 정할 수 있다.

리팩토링

리팩토링 전

많이 접해 봤을 만한 사용자가 게시글을 올리는 user,post의 예제이다.
처음에 설계할 때는 post는 사용자를 알아야 하기에 post-user의 다대일 관계는 무조건 필요하고, user도 자신의 게시글 목록을 보는 기능이 필요했기에 @ManytoOne,@OnetoMany의 양방향 관계가 필요하다 판단했다.

하지만 생각해보면 user와 post는 생성/소멸 시기도 다르고, 영구적일 정도로 강한 연관관계를 맺기에는 도메인적 공유사항도 크지 않다.
post는 작성자를 조회해야 하고 user는 자신의 글목록을 조회해야 하는 것만으로 연관관계를 맺는 것은 짧은 생각이었던 것 같다.

아래가 그 코드이다.

user

package com.movieinfo.sharewatch.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.movieinfo.sharewatch.domain.BaseTimeEntity;
import com.movieinfo.sharewatch.domain.posts.Posts;
import com.sun.istack.NotNull;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.Email;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.CascadeType.ALL;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "users", uniqueConstraints = {
        @UniqueConstraint(columnNames = "email")
})
@Entity
public class User extends BaseTimeEntity {

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

    @Column(nullable = false)
    private String name;

    @Email
    @Column(nullable = false)
    private String email;

    @Column
    private String imageUrl;

    @JsonIgnore
    private String password;

    @NotNull
    @Enumerated(EnumType.STRING)
    private AuthProvider provider;

    private String providerId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder.Default
    @OneToMany(mappedBy = "user", cascade = ALL, orphanRemoval = true)
    private List<Posts> postList = new ArrayList<>();
    
    @Builder
    public User(String name, String email, String imageUrl, String password, AuthProvider provider, String providerId, Role role) {
        this.name = name;
        this.email = email;
        this.imageUrl = imageUrl;
        this.password = password;
        this.provider = provider;
        this.providerId = providerId;
        this.role = role;
    }



    public void addPost(Posts post){
        //post의 writer 설정은 post에서 함
        postList.add(post);
    }

    public User update(String name, String email, String imageUrl) {
        this.name = name;
        this.email=email;
        this.imageUrl=imageUrl;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }

}

post

package com.movieinfo.sharewatch.domain.posts;

import com.movieinfo.sharewatch.domain.BaseTimeEntity;
import com.movieinfo.sharewatch.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {


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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @Column(length = 255,nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT",nullable = false)
    private String content;
    @Column(columnDefinition = "integer default 0")
    private int count;
    @Enumerated(EnumType.STRING)
    private Status status;

    @Builder
    public Posts( String title, Long writer_id, String content, Status status) {
        this.title = title;
        this.content = content;
        this.count = 0;
        this.status = status;
    }

    public void confirmWriter(User user) {
        this.user = user;
        user.addPost(this);
    }

    //== 내용 수정 ==//
    public void updateTitle(String title) {
        this.title = title;
    }


    public void updateContent(String content) {
        this.content = content;
    }

}

하지만 아래와 같이 바뀔 수 있겠다.

리팩토링 후

user

package com.movieinfo.sharewatch.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.movieinfo.sharewatch.domain.BaseTimeEntity;
import com.movieinfo.sharewatch.domain.posts.Posts;
import com.sun.istack.NotNull;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.Email;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.CascadeType.ALL;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "users", uniqueConstraints = {
        @UniqueConstraint(columnNames = "email")
})
@Entity
public class User extends BaseTimeEntity {

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

    @Column(nullable = false)
    private String name;

    @Email
    @Column(nullable = false)
    private String email;

    @Column
    private String imageUrl;

    @JsonIgnore
    private String password;

    @NotNull
    @Enumerated(EnumType.STRING)
    private AuthProvider provider;

    private String providerId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

	// List<Post>를 없앰
    
    @Builder
    public User(String name, String email, String imageUrl, String password, AuthProvider provider, String providerId, Role role) {
        this.name = name;
        this.email = email;
        this.imageUrl = imageUrl;
        this.password = password;
        this.provider = provider;
        this.providerId = providerId;
        this.role = role;
    }


    public User update(String name, String email, String imageUrl) {
        this.name = name;
        this.email=email;
        this.imageUrl=imageUrl;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }

}

post

package com.movieinfo.sharewatch.domain.posts;

import com.movieinfo.sharewatch.domain.BaseTimeEntity;
import com.movieinfo.sharewatch.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {


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

	//user_id를 참조하고 필요하면 repository를 통한다. 
	@Column(name="user_id")
	private Long userId

    @Column(length = 255,nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT",nullable = false)
    private String content;
    
    @Column(columnDefinition = "integer default 0")
    private int count;
    
    @Enumerated(EnumType.STRING)
    private Status status;

    @Builder
    public Posts( String title, Long writer_id, String content, Status status) {
        this.title = title;
        this.content = content;
        this.count = 0;
        this.status = status;
    }

	//confirmWriter삭제 -> 양 객체를 수정하는 메소드가 없어 나음
    
    public void updateWriter(Long userId){
    	this.userId=userId;
    }

    //== 내용 수정 ==//
    public void updateTitle(String title) {
        this.title = title;
    }

    public void updateContent(String content) {
        this.content = content;
    }

}

여기에 userRepoistory와 postRepository를 이용해 기능을 제공하는 클래스를 작성해야 한다.

예를 들면

public class postWriterService(){
	private final PostsRepository postsRepository;
    private final UserRepository userRepository;
    
    public List<User> findPostsByUser(Long userId){
    	return postsRepository.findByUserId(userId);
    }
    
    public updatePostWriter(Long postId,Long userId){
    	Posts post=postsRepository.findById(postId);
        posts.updateWriter(userId)
   
    }
    
   	... 

}

이렇게 user과 post를 모두 참조할 필요가 있을 땐 연관관계가 아니라 양쪽의 repoistory를 이용할 수 있다. 위와 같은 클래스가 post도메인에 있다면 post도메인이 user도메인에 단방향으로 의존하는 나은 구조를 가진다.

혹은 UserPostList와 같은 인터페이스를 도메인에 새로 만들고
Post도메인에서 이를 implements해서 service에서의 UserPostList에 의존한다면 의존성의 사이클을 만들지 않고 user에서 post목록을 조회할 수가 있겠다.

사실 이제야 의존성,도메인의 분리에 대해 생각해보기 시작한 시점이라 위 내용들을 제외하고도 고칠부분이 많을 것 같다. 이 포스트도 계속해서 리팩토링해보며 수정/추가해야겠다.

0개의 댓글