JPA 연관관계 정리 / spring DI Container (항해일지 30일차)

김형준·2022년 6월 7일
1

TIL&WIL

목록 보기
30/45
post-thumbnail

1. JPA 연관 관계 정리


1) JPA 연관관계 양방향 매핑

  • 단방향 매핑에서 나아가 반대 방향으로 조회 (객체 그래프 탐색) 기능이 추가된 것 뿐이다.

  • 단방향 매핑을 잘하고 필요할 때 추가하는 방식으로 진행하면 된다.

  • 사진과 글의 출처는 김영한님의 JPA 특강

  • 양방향 매핑은 양 쪽 객체 모두 상대 객체의 정보를 필드에 두는 것을 의미한다.
  • 위 그림에서 보다시피, 객체 연관관계에는 서로를 필드에 두지만
  • DB 테이블 연관관계는 단방향 매핑과 달라진 점이 없다.
    • 즉, DB 테이블에서는 PK, FK가 설정된 이상 더이상 처리해주지 않아도 양방향으로 값을 가져올 수 있기 때문이다.
    • 구현 방식은 아래와 같다. (N:1인 경우)
        1. main(주인)이 되는 객체에 @JoinColumn(name=FK) 넣어주기 (여기까지는 단방향과 동일)
        1. ✨ sub 객체의 연관관계 어노테이션에 (mappedBy = "main에 넣은 sub 변수 명") 넣어주기. (이 때, DB에는 따로 저장되지 않으나 자바 객체에는 저장됨)
    • 즉, 객체 연관관계는 위 그림에서 보다시피 회원 -> 팀 (단방향 1개) + 팀 -> 회원 (단방향 1개) 으로 이루어지고,
      • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
    • 테이블 연관관계는 회원 <-> 팀의 양방향 연관관계 1개로 이루어진다.
      • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리 (등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능 (주의❗❗ 값을 변경해도 반영이 안된다.)
    • 내가 과제를 구현하며 마주했던 NullPointerException도 sub쪽에 값을 넣어줘서 반영이 안되었던 것이다..!
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인을 명시 (mappedBy = "main에 넣은 sub 변수 명")
  • +) 양방향 매핑 시 연관관계의 주인에 값을 입력하되, sub에도 주인을 넣어주자
team.getMembers().add(memeber);
member.setTeam(team);

그렇다면 주인은 어떻게 정할까?

  • 외래키가 있는 곳을 주인으로 정해라
  • 주인의 값을 바꾸면 sub에도 update문이 날아간다.
  • Tip
    • 설계 단계에서 단방향만 고려하여 구현해라
    • 단방향으로만 설계 구현이 끝나면, 실제 개발 단계에서 양방향이 꼭 필요해질 경우에만 구현해라
    • 단방향 매핑으로 끝내는 것이 가장 간단하고 깔끔한 것

2) JPA 연관관계 분석

  • 결론: To 뒤에 One이 오는 지 Many가 오는 지를 기준으로 보면 된다.

  • @~ToOne: @JoinColumn 없이도 FK column이 주인 테이블에 생성된다.

    • N:1의 경우 보통 Many에 해당하는 객체에서 @JoinColumn(name="")을 붙이며 FK column 명을 지정해줄 수 있다.
    • 만약 @JoinColumn(name="")이 없다면 자동으로 FK 컬럼이 생성된다.
  • @~ToMany: ToMany는 해당 테이블에 여러 FK들이 담겨야 하므로 따로 연관관계 테이블이 생성된다. (객체 테이블에 FK 컬럼값은 따로 생성 안된다.)

    • ex) ORDERS_FOODS (N:N), MENU_FOODS (1:N)

    • 1:N의 경우 @OneToMany(mappedBy = " ")가 없다면 자동으로 연관관계 테이블을 생성한다.

      • 만약 연관관계 테이블 없이 1:N 양방향 매핑을 해주고 싶다면, @OneToMany(mappedBy = " ")로 readOnly 속성의 자바 객체로 필드에 넣어줄 수 있다.
      • 이는 DB에 저장되는 값이 아니며, 해당 값을 변경해도 변경사항이 반영되지 않는다.
      • 해당 변수에 값을 넣는 과정은 구현해줘야 한다. (저절로 되는 줄 알았다..)
    • N:N의 경우 연관관계 테이블은 무조건 생성된다. 두 객체 모두 @~ToMany 이므로 FK 값이 여러개일 수 있기 때문이다.

      • 이렇게 생성되는 테이블은 아래와 같이 커스터마이징 할 수 있다.
    @ManyToMany
    @JoinTable( //생성될 연관관계 테이블
            name = "ORDER_FOOD", // 테이블 이름
            joinColumns = @JoinColumn(name = "ORDER_ID"), // 해당 객체의 PK값 저장할 column 명
            inverseJoinColumns = @JoinColumn(name = "FOOD_ID") // 상대 객체의 PK값(해당 객체의 FK) 저장할 column 명
    )
    private List<Food> foods = new ArrayList<>();
  • 만약 연관관계 테이블에 PK 외에 다른 값이 필요하다면 연관관계 테이블 객체를 새로 만들어서 N:N 관계를 1:N + N:1 관계로 풀어줘야 한다.
  • 아래는 예시 코드, 아직 N:N 관계를 풀어서 정리하진 못했다. 추후에 확장성을 위해 따로 연관관계 테이블을 객체화하여 구현해볼 예정이다!
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
  @Id
  @ManyToOne
  @JoinColumn(name = "MEMBER_ID")
  private Member3 member; // MemberProduct.member와 연결

  @Id
  @ManyToOne
  @JoinColumn(name = "PRODUCT_ID")
  private Product2 product; // MemberProduct.product와 연결

  private int orderAmount;

  @Temporal(TemporalType.DATE)
  private Date orderDate;
}

3) 과제 적용

  • 위에서 정리한 결론을 토대로 무지성으로 사용했던 연관관계 어노테이션들을 정리했다.
  • 대부분 단방향 구성으로 깔끔하게 정리했고,
  • Order와 Food는 ManyToMany로
  • Food와 Menu는 ManyToOne으로 정리하였고, 연관관계 편의 메서드를 사용하여 구현했다.
    @ManyToOne
//    @JoinColumn(name="MENU_ID") -> 안해줘도 ToOne이기 때문에 자동으로 FK 컬럼이 생성된다.
    private Menu menu;
  • 연관관계 편의 메서드
    public void setMenu(Menu menu) {
        this.menu = menu;
        menu.getFoods().add(this);
    }
  • 하나의 메소드에서 양측에 관계를 설정하게 해주는 것이 안전하다.
  • 이렇게 한번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드 라 부른다.

2. 스프링 핵심 개념 강의

출처: 인프런 김영한님의 스프링 핵심 원리 기본편 강의

1) 기존 코드의 문제점 (OCP, DIP 위배)

  • 각 클래스의 필드에서 인터페이스를 넣어 의존했는데, 추상화 부분 뿐만 아니라 구체화 부분도 의존했다.
public class OrderServiceImpl implements OrderService{
    // OCP, DIP 위반!!!!!!!!!!!!!!!
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  • 위 코드를 예로 들면, OrderServiceImpl은 주문과 관련된 기능만 수행해야 하는데,
  • 할인 정책의 선택과 저장소의 선택이라는 기능까지 수행하게 되어, OCP(확장에는 열려있으나 변경에는 닫혀 있어야 한다.)에 위배된다.
  • 또한 직접 생성자를 통해 구체화를 선택함으로써, DIP(의존 관계 역전 원칙: 추상화에만 의존해라!)에 위배된다.

해결 방안

  • 각각의 클래스의 필드에는 초기화 하지 않은 인터페이스(추상화) 변수만을 선언하고,
  • 생성자를 통해 클래스의 파라미터 자리에 구체화된 구현체를 받아와서 적용시킨다.
  • 따라서 해당 생성자에 인수를 넣어 던져 줄 외부 클래스가 필요하다.
  • 이를 통해 의존관계에 대한 고민은 외부에게 맡기고 실행에만 집중할 수 있다.
public class AppConfig {

    // AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    // AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다.

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
    }
}
  • 너무 딱딱하다면 예시를 통해 생각해보면 좋다.
    • 애플리케이션을 공연으로 치환하여 각각의 배우의 역할을 인터페이스, 배우를 구현체로 치환한다.
    • 공연은 로미오와 줄리엣이고, 로미오 역할은 디카프리오가 맡았다고 가정한다.
    • 이 때, 로미오 역할의 구현체인 디카프리오가 줄리엣 역할을 맡을 구현체인 배우를 선정하겠다고 한다.
    • 이는 본인의 역할을 벗어나 공연 기획의 기능까지 수행하려고 하는 것이다.
    • 따라서 공연 기획자가 구현체 디카프리오에게 '너의 줄리엣은 xxx야' 라고 정해준다면, 디카프리오는 본인의 역할에만 충실하고, 나머지 기능은 외부에서 수행하는 것이 된다.

2) DIP 완성

  • 객체의 생성과 연결은 Appconfig가 담당한다.
  • DIP 완성: MemberServiceImplMemberRepository인 추상에만 의존하면 된다.
  • 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

  • 클라이언트인 MemberServiceImpl의 입장에서 보면 의존 관계를 마치 외부에서 주입해주는 것 같다고 해서 DI (의존성 주입, 의존 관계 주입) 이라고 한다.


AppConfig 리팩터링

  • 중복을(new 연산 반복 사용) 제거하고, 역할에 따른 구현이 보이도록 리팩터링.
  • 아래의 코드를 보면 이제 new 연산은 메서드로 대체되었고, 변경사항은 해당 메서드에만 적용시켜주면 된다.
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    // 생성자를 반환하는 메서드로 리팩터링
    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    // 생성자를 반환하는 메서드로 리팩터링
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }

3) IoC, DI, Container

-IoC

  • 위와 같이 프로그램에 대한 제어 흐름에 대한 권한이 외부에 있는 것을 IoC 라고 한다.

  • 프레임워크 vs 라이브러리

    • 프레임워크는 내가 작성한 코드를 제어하고, 대신 실행한다.
    • 반면 라이브러리는 내가 작성한 코드가 직접 제어의 흐름을 담당한다.
  • DI

    • 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
      • 정적인 클래스 의존 관계는 클래스의 import 부분만 봐도 알 수 있는 의존관계를 뜻한다.
      • 동적인 객체 인스턴스 의존 관계는 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계를 뜻한다.
  • IoC/DI Container

    • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 한다.
    • 혹은 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.

4) Spring 으로 전환하기

  • AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 스프링 컨테이너를 활용할 수도 있다.

  • [애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너 만들기]
  • 먼저, AppConfig에 @Configuration을 붙여주고, 각각의 메서드 마다 @Bean을 붙여준다.
  • 필요한 위치에서 AnnotationConfigApplicationContext를 생성하고 인수에 AppConfig.class를 넣어준다.
  • 생성한 applicationContext 에서 getBean()을 통해 가져온다, 첫번 째 인수는 메서드 명, 두번 째 인수는 반환 타입을 넣어준다.
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
  • ApplicationContext를 스프링 컨테이너라고 한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
    • 여기에서 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. (Key는 메서드명, Value는 반환 값)
  • 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
    • applicationContext.getBean("메서드 명", "반환타입") 을 통해 스프링 빈을 찾아올 수 있다.

5) 스프링 컨테이너와 스프링 빈

  • 위와 같이 Config class를 생성한 후, applicationContext의 생성자 파라미터로 넘겨주면 해당 class에 @Bean이 붙은 메서드를 스프링 빈으로 등록한다.

  • 이 때 @Bean(name="")으로 빈의 이름을 직접 부여할 수도 있다. (디폴트는 메서드명)

  • 단, 빈의 이름은 unique 해야한다!

  • 스프링 빈을 등록하는 과정에서 설정 정보를 참고하여 빈 간 의존관계를 주입한다.


6) 스프링 컨테이너 조회 방법

  • 🔗 코드 GitHub

  • 상속관계일 경우: 부모 타입으로 조회하면 자식 타입도 함께 조회한다.


5) BeanFactory와 ApplicationContext

  • BeanFactory

    • 스프링 컨테이너의 최상위 인터페이스다.
    • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
    • getBean() 을 제공한다.
    • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
  • ApplicationContext

    • BeanFactory 기능을 모두 상속받아서 제공한다.

    • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이가 뭘까?

    • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.

      • 메시지소스를 활용한 국제화 기능
      • 환경변수: 로컬, 개발, 운영등을 구분해서 처리
      • 애플리케이션 이벤트
      • 편리한 리소스 조회
    • ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다!


profile
BackEnd Developer

0개의 댓글