인프런 커뮤니티 질문&답변

최영원님의 프로필 이미지

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

양방향 연관관계에서 연관관계 편의 메서드 위치에 대해 질문드립니다.

해결된 질문

22.11.28 20:56 작성

·

740

0


[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (아니오)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]
안녕하세요. 좋은 강의 덕분에 양질의 지식을 비교적 수월하게 습득하고 있습니다.

양방향 연관관계에서 편의 메서드는 어디에 위치하는게 좋은지 여쭙고자 합니다.
(비슷한 질문들을 봤지만 이러한 경우에 어떤게 가장 좋은 선택일지 궁금하여 질문드립니다.)
비슷한 질문1 : https://www.inflearn.com/questions/16308
비슷한 질문2 : https://www.inflearn.com/questions/99330

 

현재상태

위와 같이 일대다(1:N) 관계가 있고 Team 을 애그리거트 루트로 잡았습니다.

도메인 룰은 다음과 같습니다.

  1. Team 은 반드시 이름이 있어야한다.

  2. Player는 반드시 이름과 나이가 있어야한다.

  3. Player는 반드시 Team에 소속되어야하며, 하나의 Team 에만 소속될 수 있다.

  4. playerTeam 은 변경될 수 없다.

엔티티 코드 입니다. (연관관계 편의 메서드를 애그리거트 루트쪽에 두었습니다.)
(두 엔티티는 동일한 패키지에 있습니다. package com.example.jpa.module.team.domain;)

@Entity
public class Team {
  
  @Id @GeneratedValue
  private Long teamNo;

  private String name;

  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
  private List<Player> players = new ArrayList<>();

  //생성자
  public Team(String name) {
    this.name = name;
  }

  //연관관계 편의 메서드
  public void addPlayer(Player player) {
    player.setTeam(this);
    this.players.add(player);
  }
}
@Entity
public class Player {

  @Id @GeneratedValue
  private Long playerNo;

  private String name;

  private Integer age;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "team_no")
  private Team team;

  //생성자
  public Player(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void setTeam(Team team) {
    this.team = team;
  }
	
}

service 계층 코드입니다.

Team team = new Team("team1");
Player player = new Player("kim", 24);
team.addPlayer(player);
teamRepository.save(team);

Team 이 Player 를 완전히 소유하는 관계이기 때문에 애그리거트 루트를 Team 으로 잡았고 연관관계 메서드 또한 Team 에 있는게 자연스러워 보입니다.
(예시가 Team-Player라 결합도가 좀 떨어지는것 처럼 느껴지는데 주문-주문항목 이나 게시글-첨부파일 같은 관계로 봐주시면 감사하겠습니다.)

문제점

  1. 연관관계 편의 메서드 addPlayer(..)가 호출되기 전까지 Player의 도메인 룰이 깨진 상태가 됩니다.

    • 도메인 룰 3번 "Player 는 반드시 Team에 소속되어야 한다" 를 위반하게 됩니다.

  2. Player의 setTeam(..) 메서드를 클래스 외부에 공개해야 합니다.

    • 같은 패키지라 public 으로 공개하진 않았지만 해당 메서드로 인해
      도메인 룰 4번 "player 의 Team 은 변경될 수 없다." 를 위반할 여지가 생겼습니다.

    • setTeam(..) 메서드에 this.team != null 인 경우 예외를 발생시켜 도메인 룰을 지킬 수 있지만 좋은 방법인지 잘 모르겠습니다..

시도한 방법

Player의 생성자에 Team을 받도록 합니다.

public Player(Team team, String name, int age) {
  this.team = team;
  this.name = name;
  this.age = age;
}

근데 이렇게 하니 그냥 연관관계 편의 메서드를 Player에 두는게 나은것 같아
생성될때 연관관계를 맺도록 정적 팩토리 메서드를 작성했습니다.

public static Player createAndLink(Team team, String name, int age) {
  Player player = new Player(team, name, age); //생성자 private 으로 변경
  team.getPlayers().add(player); //반대편 연관관계 설정
  return player;
}

일단 도메인 룰은 모두 만족하는 듯 보이나 service 계층 코드가 뭔가 부자연스럽습니다.

Team team = new Team("team1");
Player player = Player.createAndLink(team, "kim", 24);
teamRepository.save(team);

위 코드를 보면 player를 생성하고 사용하지 않는것 처럼 보여 불필요한 코드로 인식됩니다.
(인텔리제이에서도 player가 미사용 중이라고 나옵니다.)


혹시 위와 같은 상황에서 어떻게 해결하는게 가장 좋은 방법일까요?
(긴 글 읽어주셔서 감사합니다.)

답변 2

1

김영한님의 프로필 이미지
김영한
지식공유자

2022. 12. 04. 22:56

안녕하세요. 최영원님

저도 딱 좋은 방법이 떠오르지 않네요. 참고로 저는 생성자나 정적 메서드를 써서 문제가 깔끔하게 해결되지 않으면 문제점 1,2 정도는 그냥 안고 진행하는 편입니다.

더 나은 좋은 방법이 있는 분들은 도움을 주시면 좋겠습니다.

감사합니다.

0

최영원님의 프로필 이미지
최영원
질문자

2022. 12. 06. 21:46

강사님 답변 정말 감사합니다.

여러가지 방법을 시도해봤는데 애그리거트 루트에 연관관계 편의 메서드를 두고 강사님 말씀처럼 문제점 1,2 번은 그냥 안고가는게 제일 괜찮은 방법인것 같습니다.

애그리거트 루트에 편의 메서드를 둠으로써 응용계층 코드도 자연스러워져 코드 흐름을 파악하기가 더 좋은것 같습니다.


@Entity
public class Team {
  
  @Id @GeneratedValue
  private Long teamNo;

  private String name;

  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
  private List<Player> players = new ArrayList<>();

  //생성자
  public Team(String name) {
    this.name = name;
  }

  //연관관계 편의 메서드
  public void addPlayer(Player player) {
    Assert.notnull(player, "player is null");
    player.initTeam(this);
    this.players.add(player);
  }
}
@Entity
public class Player {

  @Id @GeneratedValue
  private Long playerNo;

  private String name;

  private Integer age;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "team_no")
  private Team team;

  //생성자
  public Player(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void initTeam(Team team) {
    Assert.notnull(team, "team is null");
    Assert.state(this.team == null, "이미 Team에 소속되어 있습니다.");
    this.team = team;
  }
	
}
Team team = new Team("team1");
Player player = new Player("kim", 24);
team.addPlayer(player);
teamRepository.save(team);