✍️
dodeon
  • 개발왕, 도던
  • 스프링 시큐리티 인 액션
    • 오늘날의 보안
    • 안녕! 스프링 시큐리티
    • 사용자 관리
    • 암호 처리
    • 인증 구현
    • 실전: 작고 안전한 웹 애플리케이션
    • 권한 부여 구성: 액세스 제한
    • 권한 부여 구성: 제한 적용
    • 필터 구현
    • CSRF 보호와 CORS 적용
    • 실전: 책임의 분리
    • OAuth 2가 동작하는 방법
    • OAuth 2: 권한 부여 서버 구현
  • 스프링 고급편
    • 스레드 로컬
    • 템플릿 메서드 패턴과 콜백 패턴
    • 프록시 패턴과 데코레이터 패턴
  • 스프링 입문
    • 프로젝트 환경설정
    • 스프링 웹 개발 기초
    • 회원 관리 예제 - 백엔드
    • 스프링 빈과 의존 관계
    • 회원 관리 예제 - MVC
    • 스프링 DB 접근 기술
      • JDBC
      • JPA
    • AOP
  • 스프링 핵심 원리
    • 객체 지향 설계와 스프링
      • 스프링의 탄생
      • 객체 지향 프로그래밍
      • 좋은 객체 지향 설계의 원칙
      • 객체 지향 설계와 스프링
    • 스프링 핵심 원리 이해
      • 회원 도메인 개발
      • 주문 도메인 개발
    • 객체 지향 원리 적용
      • 관심사의 분리
      • 새로운 구조와 정책 적용
      • 정리
      • IoC, DI, 컨테이너
      • 스프링으로 전환하기
    • 스프링 컨테이너와 스프링 빈
      • 스프링 빈 기본 조회
      • 동일 타입이 둘 이상일 때 조회
      • 상속일 때 조회
      • BeanFactory와 ApplicationContext
      • 다양한 설정 형식
      • 스프링 빈 설정 메타 데이터
    • 싱글턴 컨테이너
      • @Configuration과 싱글턴
    • 컴포넌트 스캔
      • 탐색 위치와 기본 탐색 대상
      • 필터와 중복 등록
    • 의존 관계 자동 주입
      • 롬복과 최신 트렌드
      • 조회 빈이 2개 이상일 때
      • 애너테이션 직접 만들기
      • 조회한 빈이 모두 필요할 때
      • 올바른 실무 운영 기준
    • 빈 생명 주기 콜백
      • 인터페이스 방식
      • 메서드 지정 방식
      • 애너테이션 방식
    • 빈 스코프
      • 프로토타입 스코프
      • Provider
      • 웹 스코프
  • 스프링 MVC
    • 웹 애플리케이션 이해
      • 서버
      • 서블릿
      • 멀티 스레드
      • HTML, HTTP API, CSR, SSR
      • 자바 백엔드 웹 기술 역사
    • 서블릿
      • HttpServletRequest
      • HTTP 요청 데이터
      • HttpServletResponse
      • HTTP 응답 데이터
    • 서블릿, JSP, MVC 패턴
      • 서블릿으로 만들기
      • JSP로 만들기
      • MVC 패턴
    • MVC 프레임워크 만들기
      • 프론트 컨트롤러 패턴
      • View 분리
      • Model 추가
      • 단순하고 실용적인 컨트롤러
      • 유연한 컨트롤러
      • 정리
    • 스프링 MVC의 구조 이해
      • 스프링 MVC 전체 구조
      • 핸들러 매핑과 핸들러 어댑터
      • 뷰 리졸버
      • 스프링 MVC 시작하기
      • 스프링 MVC 컨트롤러 통합
      • 스프링 MVC 실용적인 방식
    • 스프링 MVC 기본 기능
      • 프로젝트 생성
      • 로깅
      • 요청 매핑
      • HTTP 요청의 기본 및 헤더 조회
      • HTTP 요청 파라미터
      • HTTP 요청 메시지
      • HTTP 응답
      • HTTP 메시지 컨버터
      • 요청 매핑 핸들러 어댑터
    • 스프링 MVC 웹 페이지 만들기
    • 메시지, 국제화
      • 스프링 메시지 소스
    • Validation
      • BindingResult
      • FieldError, ObjectError
      • 오류 코드와 메시지 처리
      • Validator 분리
    • Bean Validation
      • Form 전송 객체 분리
      • HTTP 메시지 컨버터
    • 로그인
      • 쿠키
      • 세션
      • 서블릿 HTTP 세션
      • 서블릿 필터
      • 스프링 인터셉터
      • ArgumentResolver 활용
    • 예외 처리와 오류 페이지
      • 오류 화면 제공
      • 필터
      • 인터셉터
      • 스프링 부트 오류 페이지
    • API 예외 처리
      • 스프링 부트 기본 오류 처리
      • HandlerExceptionResolver
      • ExceptionResolver
      • ControllerAdvice
    • 스프링 타입 컨버터
      • Converter
      • ConversionService
      • 뷰 템플릿에 적용하기
      • Formatter
    • 파일 업로드
      • 서블릿과 파일 업로드
      • 스프링과 파일 업로드
      • 파일 업로드 및 다운로드 예제
  • 자바 ORM 표준 JPA 프로그래밍
    • JPA 소개
    • JPA 시작하기
    • 영속성 관리
      • 영속성 컨텍스트
      • 플러시
      • 준영속 상태
    • Entity 매핑
      • 객체와 테이블 매핑
      • 데이터베이스 스키마 자동 생성
      • 필드와 칼럼 매핑
      • 기본 키 매핑
      • 실전 예제
    • 연관 관계 매핑
      • 단방향 연관 관계
      • 양방향 연관 관계
      • 실전 예제
    • 다양한 연관 관계 매핑
      • 다대일
      • 일대다
      • 일대일
      • 다대다
      • 실전 예제
    • 고급 매핑
      • 상속 관계 매핑
      • 매핑 정보 상속
      • 실전 예제
    • 프록시와 연관관계 관리
      • 프록시
      • 즉시 로딩과 지연 로딩
      • 영속성 전이와 고아 객체
      • 실전 예제
    • 값 타입
      • 기본값 타입
      • 임베디드 타입
      • 값 타입과 불변 객체
      • 값 타입의 비교
      • 값 타입 컬렉션
      • 실전 예제
    • 객체 지향 쿼리 언어 - 기본
      • 기본 문법과 쿼리 API
      • 프로젝션
      • 페이징
      • 조인
      • 서브 쿼리
      • JPQL 타입 표현과 기타 식
      • 조건식
      • JPQL 함수
    • 객체 지향 쿼리 언어 - 중급
      • 경로 표현식
      • fetch join
      • 다형성 쿼리
      • Entity 직접 사용
      • Named 쿼리
      • 벌크 연산
  • 스프링 부트와 JPA 활용 - 웹 애플리케이션 개발
    • 프로젝트 환경설정
    • 도메인 분석 설계
      • 도메인 분석 설계
      • Entity 클래스 개발
      • Entity 설계 시 주의점
    • 애플리케이션 아키텍처
    • 회원 도메인 개발
    • 상품 도메인 개발
    • 주문 도메인 개발
      • Entity, 리포지토리, 서비스 개발
      • 주문 기능 테스트
      • 주문 검색 기능 개발
    • 웹 계층 개발
      • 변경 감지와 병합
  • 스프링 부트와 JPA 활용 - API 개발과 성능 최적화
    • API 개발 기본
      • 회원 등록 API
      • 회원 수정 API
      • 회원 조회 API
    • 지연 로딩과 조회 성능 최적화
      • Entity 직접 노출
      • Entity를 DTO로 변환
      • JPA에서 DTO 직접 조회
    • 컬렉션 조회 최적화
      • Entity 직접 노출
      • Entity를 DTO로 변환: 페치 조인
      • Entity를 DTO로 변환: 페이징과 한계 돌파
      • DTO 직접 조회
      • DTO 직접 조회: 컬렉션 조회 최적화
      • DTO 직접 조회: 플랫 데이터 최적화
      • 정리
    • OSIV와 성능 최적화
  • 스프링 데이터 JPA
    • 예제 도메인 모델
    • 공통 인터페이스 기능
      • 순수 JPA 기반 리포지토리
      • 공통 인터페이스 설정
    • 쿼리 메서드 기능
      • JPA Named Query
      • @Query
      • 파라미터 바인딩
      • 반환 타입
      • 페이징과 정렬
      • 벌크성 수정 쿼리
      • @EntityGraph
      • JPA Hint & Lock
    • 확장 기능
      • 사용자 정의 리포지토리
      • Auditing
      • Web 확장
    • 스프링 데이터 JPA 분석
    • 나머지 기능
      • Specifications
      • Query By Example
      • Projections
      • Native Query
  • Querydsl
    • 프로젝트 환경 설정
    • 예제 도메인 모델
    • 기본 문법
      • JPQL vs Querydsl
      • Q-Type 활용
      • 검색 조건
      • 결과 조회
      • 정렬
      • 페이징
      • 집합 함수
      • 조인
      • 서브 쿼리
      • Case 문
      • 상수, 문자 더하기
    • 중급 문법
      • 프로젝션과 결과 반환
      • 동적 쿼리
      • 수정, 삭제 벌크 연산
      • SQL Function 호출
    • 순수 JPA와 Querydsl
      • 순수 JPA 리포지토리와 Querydsl
      • 동적 쿼리와 성능 최적화 조회
      • 조회 API 컨트롤러 개발
    • 스프링 데이터 JPA와 Querydsl
      • 스프링 데이터 페이징 활용
      • 스프링 데이터 JPA가 제공하는 Querydsl 기능
  • 데이터 접근 핵심 원리
    • JDBC 이해
      • JDBC와 최신 데이터 접근 기술
      • 데이터베이스 연결
      • JDBC 개발
  • 백엔드 시스템 실무
    • CPU bound 애플리케이션
      • CPU를 극단적으로 사용하는 애플리케이션
      • 스트레스 테스트 툴로 성능 측정
      • Dockerized 애플리케이션 GCP 배포
      • Jenkins를 이용한 배포
    • CPU bound 애플리케이션 무중단 배포
      • nginx를 통한 로드밸런싱 구성
      • 서버를 늘려서 성능 측정
    • 배포 자동화와 협업을 위한 Git
      • GitHub Webhook과 jenkins로 배포 자동화
      • 머지할 때 발생하는 충돌 해결하기
      • 실무에서 유용한 Git 꿀팁
    • I/O bound 애플리케이션
    • Message Queue를 도입하여 데이터 유실 방지
      • 스트레스 테스트
    • 검색과 분석을 위한 저장소 ElasticSearch
    • Kubernetes
  • 모든 개발자를 위한 HTTP 웹 기본 지식
    • 인터넷 네트워크
      • IP
      • TCP, UDP
      • PORT
      • DNS
    • URI와 웹 브라우저 요청 흐름
    • HTTP 기본
      • 클라이언트-서버 구조
      • stateful, stateless
      • 비 연결성
      • HTTP 메시지
    • HTTP 메서드
    • HTTP 메서드 활용
    • HTTP 상태 코드
    • HTTP 헤더 - 일반
      • 표현
      • 콘텐츠 협상
      • 전송 방식
      • 정보
      • Authorization
      • 쿠키
    • HTTP 헤더 - 캐시
      • 검증 헤더와 조건부 요청
      • 조건부 요청 헤더
      • 프록시 캐시
      • 캐시 무효화
  • 김영한의 실전 자바
    • 제네릭
  • 예제로 배우는 스프링 입문
    • 예제로 배우는 스프링 입문(개정판)
      • PetClinic 예제
      • 스프링 IoC
      • 스프링 AOP
      • 스프링 PSA
  • 스프링 프레임워크 핵심 기술
    • 스프링 프레임워크 핵심 기술
      • IoC 컨테이너와 빈
        • 스프링 IoC 컨테이너와 빈
        • ApplicationContext와 빈 설정
        • @Autowired
        • @Component와 컴포넌트 스캔
        • 빈의 스코프
        • Environment
        • MessageSource
        • ApplicationEventPublisher
        • ResourceLoader
      • Resource/Validation
        • Resource 추상화
        • validation 추상화
      • 데이터 바인딩 추상화
      • SpEL
      • 스프링 AOP
      • Null-Safety
  • 스프링 부트 개념과 활용
    • 스프링 부트 원리
      • 자동 설정
      • 내장 서버
        • 컨테이너와 서버 포트
        • HTTPS와 HTTP2
      • 독립적으로 실행 가능한 JAR
    • 스프링 부트 활용
      • Spring Application
      • 외부 설정
      • 프로파일
      • 로깅
      • 테스트
      • Spring Boot Devtools
    • 스프링 웹 MVC
      • 소개
      • HttpMessageConverters
      • ViewResolver
      • 정적 리소스
      • 웹 JAR
      • index 페이지와 파비콘
      • ExceptionHandler
      • Spring HATEOAS
      • CORS
  • THE JAVA
    • JVM 이해하기
      • 자바, JVM, JDK, JRE
      • JVM 구조
      • 클래스 로더
      • Heap
      • Garbage Collector
    • 리플렉션
      • 클래스 정보 조회
  • The Java - Test
    • JUnit 5
      • JUnit 시작하기
      • JUnit 시작하기
    • Mockito
Powered by GitBook
On this page
  • 연관 관계의 주인과 mappedBy
  • 연관 관계의 주인
  • 주인을 결정하는 기준
  • Tip
  • 양방향 매핑 시 가장 많이 하는 실수
  • 연관 관계 주인에 값을 입력하지 않음
  • 양쪽에 모두에 값을 세팅하지 않음
  • 연관 관계 편의 메서드
  • 무한 루프
  • 정리

Was this helpful?

  1. 자바 ORM 표준 JPA 프로그래밍
  2. 연관 관계 매핑

양방향 연관 관계

Previous단방향 연관 관계Next실전 예제

Last updated 3 years ago

Was this helpful?

  • 양방향으로 한다고 해도 테이블 연관 관계는 변한 것이 없다.

    • 테이블은 member에서 team을, team에서 member를 외래키 하나로 join 해서 자유롭게 조회할 수 있다.

  • 하지만 객체는 team에 List<Member> members를 넣어줘야만 접근이 가능하다.

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;
    private int age;

    @ManyToOne
    @Column(name = "TEAM_ID")
    private Team team;
}
@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // mappedBy는 Member에 있는 'team' 변수가 연결되어 있음을 의미한다.
    // 즉, 반대편에 무엇이 걸려있는지 알려준다.
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>(); // 리스트는 초기화 하는 것이 관례다. add 할 때 NPE를 방지한다.

}
public class JpaMain() {

    public static void main(String[] args) {
        ...

        Team findTeam = em.find(Team.class, team.getId());

        // 이제 team에서도 member를 조회할 수 있다.
        int memberSize = findTeam.getMembers().size();
    }
}

연관 관계의 주인과 mappedBy

  • 객체는 연관 관계가 2개다.

    • 회원에서 팀으로

    • 팀에서 회원으로

    • 단방향이 2개 있는 것이다.

  • 테이블은 연관 관계가 1개다.

    • 회원과 팀 사이의 양방향

    • member 테이블의 FK와 team의 PK를 조인하면 된다.

  • 테이블 연관 관계는 FK 하나로 끝이 나지만, 객체는 참조가 두 곳에 다 있어야 한다.

  • 객체의 양방향 관계는 사실 양방향이 아니라, 서로 다른 단방향 관계가 2개인 상태다.

  • 객체를 양방향으로 참조하려면 단방향 연관 관계를 2개 만들어야 한다.

  • 하지만 테이블은 외래키 하나로 두 테이블의 연관 관계를 관리한다.

  • 외래키 하나로 양방향 연관 관계를 갖고 있으므로 양쪽으로 조인할 수 있다.

  • member를 새로운 team에 넣고 싶은데 member에서 team 값을 바꿔야할지 team에서 members 값을 바꿔야할지 딜레마가 온다.

    • member의 team 값을 수정했을 때 외래키 값이 업데이트 되어야 할까,

    • 아니면 team의 members 값을 수정했을 때의 외래키 값이 업데이트 되어야 할까?

  • DB 입장에서는 어찌됐든 member에 있는 FK인 team_id만 업데이트 되면 된다.

  • 결국 둘 중 하나만 외래키를 관리해야 한다. 연관 관계의 주인을 정해야 하는 것이다.

    • 연관 관계의 주인은 양방향 매핑에서만 나온다.

연관 관계의 주인

  • 객체의 두 관계 중 하나를 연관 관계의 주인으로 지정한다.

  • 연관 관계의 주인

    • 외래키 등록, 수정 등을 관리한다.

    • mappedBy 속성을 사용하지 않는다.

  • 주인이 아닌 쪽

    • 읽기만 가능하다.

    • mappedBy 속성을 사용해 주인이 누군지 명시한다.

주인을 결정하는 기준

@Entity
public class Member {
    ...

    // 외래키가 있는 곳을 연관 관계의 주인으로 정한다.
    // 연관 관계 주인인 team만 insert, update를 할 수 있다.
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    ...

    // team에 의해 관리가 된다는 뜻이다. 즉, team이 연관 관계의 주인이다.
    // 따라서 읽기만 가능하다. 값을 넣어도 아무 일이 일어나지 않는다.
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}
  • 주인

    • 외래키가 있는 곳인 Many를 주인으로 정한다.

    • Member.team

  • 가짜 매핑

    • 주인의 반대편인 One 쪽을 말한다.

    • Team.members

Member.team이 주인이 되면 외래키가 Member에 있기 때문에 쿼리를 한 방에 보낼 수 있다. Member 객체를 바꿨으니까 member 테이블에 업데이트 쿼리가 나가는구나 하고 직관적으로 이해가 된다.

반대로 Team.members가 주인이 되면, members를 바꿨을 때 내가 수정한 테이블인 team이 아니라 member 테이블에 쿼리가 나가야 한다. 나는 team 객체를 수정했는데 member 테이블이라는 엉뚱한 곳에 쿼리가 나가기 때문에 혼란스럽다.

Tip

  • 헷갈리면 무조건 외래키가 있는 곳을 주인으로 정하면 된다.

  • DB 입장에서 보면 외래키가 있는 곳이 무조건 N이고 외래키가 없는 곳은 무조건 1이다.

  • 즉, N쪽이 무조건 연관 관계의 주인이 되는 것이다.

    • N쪽이 무조건 ManyToOne이 된다.

연관 관계의 주인이라고 하면 뭔가 비즈니스적으로 중요하게 느껴지지만 그것과는 전혀 상관이 없다. 그냥 N쪽인 곳이 주인이 되면 된다.

자동차와 자동차 바퀴로 비유를 하자면, 비즈니스적으로는 자동차가 중요하지만 연관 관계의 주인은 바퀴가 되어야 한다. N쪽이 연관 관계의 주인이 되면 된다.

양방향 매핑 시 가장 많이 하는 실수

연관 관계 주인에 값을 입력하지 않음

public class JpaMain {

    public static void main(String[] args) {
        ...

        // member를 하나 만든다.
        Member member = new Member();
        member.setName("member1");

        em.persist(member);

        // team에 만든 member를 추가한다.
        Team team = new Team();
        team.setName("TeamA");
        team.getMembers().add(member);

        em.persist(team);
    }
}
insert into Member(team_id, username, member_id) values (?, ?, ?)
insert into Team(name, team_id) values (?, ?)
  • insert 쿼리는 분명 2번이 나갔지만 회원에 팀 아이디가 저장되지 않았다.

  • JPA는 insert나 update할 때 읽기 전용 필드를 아예 보지 않기 때문이다.

    • 연관 관계의 주인은 Member.team이다.

    • Team.members는 mappedBy 된 읽기 전용이다.

    • Team.members에서 수정을 했으니 반영되지 않은 것이다.

public class JpaMain {

    public static void main(String[] args) {
        Team team = new Team();
        team.setName("TeamA");
        // team에 member를 추가하는 대신
        // team.getMembers().add(member);

        em.persist(team);

        Member member = new Member();
        member.setName("member1");
        // 연관 관계의 주인인 Member.team에 추가한다.
        member.setTeam(team);

        em.persist(member);
    }
}
  • 연관 관계의 주인인 곳에만 값을 넣도록 수정하면 정상 반영된다.

양쪽에 모두에 값을 세팅하지 않음

public class JpaMain {

    public static void main(String[] args) {
        Team team = new Team();
        team.setName("TeamA");

        em.persist(team);

        Member member = new Member();
        member.setName("member1");
        // 연관 관계 주인에 세팅
        member.setTeam(team);

        em.persist(member);

        em.flush();
        em.clear();

        // Team.members에 세팅해준 게 없어도 select 쿼리를 치면 데이터가 나온다.
        Team findTeam = em.find(Team.class, team.getId());
        List<Member> members = findTeam.getMembers();

        for (Member m : members) {
            System.out.println("m = " + m.getUsername());
        }
    }
}
  • 주인 쪽에만 세팅해도 반대편에 같이 적용되기 때문에 findTeam.getMembers()로 조회하면 select 쿼리가 나간다.

public class JpaMain {

    public static void main(String[] args) {
        Team team = new Team();
        team.setName("TeamA");
        em.persist(team);

        Member member = new Member();
        member.setName("member1");
        member.setTeam(team);
        em.persist(member);

        // 하지만 양쪽 다 연관 관계 설정를 추가하는 게 좋다.
        team.getMembers().add(member);

        // 만약 flush와 clear를 해주지 않으면
//        em.flush();
//        em.clear();

        // team은 em.persist(team) 해줬을 때의 상태 그대로 1차 캐시에 들어가있다.
        // 연관 관계가 적용된 데이터는 메모리에만 올라가 있다.
        // 따라서 여기서 Team.members를 조회하면 값이 나오지 않는다.
        // 영속성 컨텍스트에는 team.setName()까지만 했던 그대로 있기 때문이다.
        Team findTeam = em.find(Team.class, team.getId());
        // 따라서 getMembers()를 하면 select 쿼리가 나가지 않는다.
        List<Member> members = findTeam.getMembers();

        for (Member m : members) {
            System.out.println("m = " + m.getUsername());
        }
    }
}
  • 하지만 양쪽 다 연관 관계를 설정해주는 게 좋다.

    • flush(), clear()가 없으면 연관 관계가 반영되지 않은 데이터를 1차 캐시에서 가져올 수 있기 때문이다.

    • 객체 지향적으로도 순수 객체 상태를 고려해 항상 양쪽에 값을 설정 해주는 게 맞다.

    • JPA 없이 순수하게 동작하도록 테스트 케이스를 짰을 경우도 team.getMembers()가 빈 값으로 나오는 문제가 발생할 수 있다.

연관 관계 편의 메서드

public class Member {
    ...

    // 연관 관계 메서드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
public class JpaMain {

    public static void main(String[] args) {
        Team team = new Team();
        team.setName("TeamA");

        em.persist(team);

        Member member = new Member();
        member.setName("member1");
        // 연관 관계 편의 메서드
        member.changeTeam(team);

        em.persist(member);

        em.flush();
        em.clear();

        Team findTeam = em.find(Team.class, team.getId());
        List<Member> members = findTeam.getMembers();

        for (Member m : members) {
            System.out.println("m = " + m.getUsername());
        }
    }
}
  • 연관 관계 편의 메서드를 사용하면 양쪽에 설정하는 까먹지 않을 수 있다.

  • setTeam()에서 Member.team을 세팅할 때 member 자신도 Team.members()에 세팅한다.

  • 메서드를 원자적으로 즉, 하나만 써도 양쪽으로 적용할 수 있다.

public class Team {
    ...

    // 가짜 매핑쪽에 만든 연관 관계 편의 메서드
    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}
public class JpaMain {

    public static void main(String[] args) {
        Team team = new Team();
        team.setName("TeamA");
        em.persist(team);

        Member member = new Member();
        member.setName("member1");
        em.persist(member);

        // 연관 관계 편의 메서드
        team.addMember(member);

        em.flush();
        em.clear();

        Team findTeam = em.find(Team.class, team.getId());
        List<Member> members = findTeam.getMembers();

        for (Member m : members) {
            System.out.println("m = " + m.getUsername());
        }
    }
}
  • 연관 관계 편의 메서드는 반대로도 할 수 있다.

  • 양쪽으로 설정하면 문제가 되므로 무엇이 기준이 될지 정하자.

  • 연관 관계 주인은 계속 회원에 있는 팀이지만 값을 세팅하는 것은 자유다.

    • 중요한 건 둘 중 하나에만 해야한다는 것이다.

무한 루프

  • toString(), lombok, JSON 생성 라이브러리 등에서 문제가 된다.

public class Member {

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                // 무한 루프
                ", team='" + team + '\'' +
                '}';
    }
}
  • toString()

    • team을 출력한다.

    • team.toString()도 호출한다.

    • 그런데 team에서도 toString()에서 member를 호출한다.

    • 무한 루프에 빠진다.

  • JSON 라이브러리

    • 엔티티의 연관 관계가 양방향일 때

    • 컨트롤러에서 response로 엔티티를 직접 보내는 경우

    • JSON으로 변환할 때 무한 루프에 빠진다.

toString()은 웬만하면 쓰지 말자.

JSON 라이브러리의 경우, 컨트롤러에서 엔티티 대신 DTO로 변환해서 보내면 된다. 엔티티를 바로 반환하면 무한 루프 이슈뿐 아니라 엔티티를 변경하는 순간 API 스펙이 바뀌어 버리는 문제가 있다.

정리

  • 웬만하면 단방향 매핑으로 끝내야 한다.

    • 단방향 매핑만으로도 이미 연관 관계 매핑은 완료된다.

    • 테이블 설계를 어느 정도 하면서 객체 설계를 하므로 테이블에서 파악된 FK로 단방향 매핑을 설계해야 한다.

  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색)하는 기능을 추가한 것 뿐이다.

    • 오히려 양방향을 하면 고려할 것들만 많아진다.

      • 연관 관계 편의 메서드도 만들어야 한다.

  • 실무에서 JPQL 등으로 역방향 참조가 필요한 경우 그때 추가하면 된다.

    • 단방향 매핑을 잘 해놓은 다음, 양방향은 필요할 때 사용한다.

    • 테이블에 영향가는 것 없이 연관 관계만 설정해주면 되기 때문이다.

  • 연관 관계의 주인

    • 비즈니스 로직 기준 X

    • 외래 키의 위치 기준 O