Spring Boot 11일차

김용진·2023년 10월 11일
0
post-custom-banner

1. 양방향 연관관계

Member 테이블과 Team 테이블이 있을 경우, 외래키를 Member에 지정하면서, Member가 Team을 가졌으나, Team은 Member의 정보를 가져오지 못하게 되어있다. 이를 단방향 연관 관계로 본다.

	@ManyToOne
	@JoinColumn(name="TEAM_ID")
	@Setter(value=AccessLevel.NONE) // lombok에서 자동 setter를 막는다.
	private Team team;

Member 클래스에 다음과 같이 일대다 연관 관계를 맺어 정보를 가져오지만, Team클래스에는 별도로 지정한것이 없기에 한 방향으로만 관계를 가지고 있다.

2. 객체와 테이블이 관계를 맺는 차이

2-1. 테이블 연관관계 = 1개

  • 회원 <-> 팀의 연관관계 1개(양방향)
    -> MEMBER 테이블 입장에서 TEAM 테이블 조인 가능
    -> TEAM 테이블 입장에서 MEMBER 테이블 조인 가능

2-2. 객체 연관관계 = 2개

  • 회원 -> 팀 연관관계 1개 (단방향)
  • 팀 -> 회원 연관관계 1개 (단방향)
    -> 사실은 단방향 연관관계가 2개 이어져있는 상태, 억지로 양방향이라고 말하는 것 실제와는 다르다.

DB에서는 외래키를 지정하게 해주면 쿼리문을 통해 조인이 양쪽으로 가능하다는걸 보여주지만
자바의 객체개념으로 보면 별도의 어노테이션을 이용해서 단방향을 서로 만들어 해주어야 한다는걸 보여주는 구문

3. 둘 중 하나로 외래키를 관리해야 한다.

  • Member에서 Team으로 가는 team 참조 값과, Team에서 Member로가는 members참조 값이 있다.

  • Member에서 Team값이 수정 됐을 때 MEMBER 테이블의 TEAM_ID가 수정이 되야 하는지, Team에 있는 members를 수정 했을 때 MEMBER에 있는 TEAM_ID가 수정이 되야 하는지?

    => DB입장에서는 MEMBER에 있는 TEAM_ID만 update가 되면 된다.

즉 현재 보는것처럼 Member에 FK가 존재하기에, Member 클래스 에서 외래키를 관리한다.

4. 연관관계의 주인(Owner)

  • 양방향 매핑 규칙
    - 객체의 두 관계중 하나를 연관관계의 주인으로 지정
    - 연관관계 주인만이 외래키를 관리(등록, 수정)할 수가 있다.
    - 주인이 아닌 쪽은 읽기만 가능
    - 주인은 mappedBy 속성을 사용하지 않은 쪽이 주인이다.
    -> 내가 누군가에게 의해서 mapping이 되었다 라는 뜻
    - 주인이 아니면 mappedBy 속성으로 주인을 지정
    - mappedBy가 적힌 곳은 읽기만 가능, 즉 값을 넣어도 update가 진행되지 않는다. 대신 조회는 가능
    @OneToMany(mappedBy = "team")
	private List<Member> member = new ArrayList<>();

위 코드는 Team 클래스에 일대다 관계를 맺어주는 구문이다. 하나의 팀에 여러 멤버들이 있기 때문에, List를 선언하는 모습을 볼 수 있다.

5. 주의사항

		Member member = new Member();
		member.setUsername("member1");
		em.persist(member);
		
		Team team = new Team();
		team.setName("TeamA");
		team.getMember().add(member);
		em.persist(team);

  • DB에서 조인을 통해 값을 불러왔더니 TEAM_ID에 null로 되어있다. MEMBER 테이블에 값이 들어가 있지 않은걸 확인했다.

TEAM테이블이 먼저 지정이 되어야 TEAM_ID의 값이 들어가고, MEMBER 테이블에 TEAM_ID컬럼에 값을 넣어줄 수 있는데, 순서가 바뀌어있다. 그래서 코드에서는 team.getMember().add(member); 를 사용해서 멤버 클래스에 TEAM_ID에 값을 넣어주고 싶었으나, null값 즉, 값이 들어가지 않았음을 볼 수 있다.

거듭 말하지만 현재 관계의 주인은 Member 클래스이다. 즉, Team클래스에서는 수정할 권한이 없단걸 인지하자.

그렇다면 어떻게할까?

Member클래스(Owner) 경우

@Entity
@Getter @Setter
public class Member2 {

	@Id @Column(name="MEMBER_ID")
	@GeneratedValue
	private Long id;
	private String username;
	
	@ManyToOne
	@JoinColumn(name="TEAM_ID")
	@Setter(value=AccessLevel.NONE) // lombok에서 자동 setter를 막는다.
	private Team2 team2;
	

	public void setTeam(Team2 team2) {
		this.team2 = team2;
	}
	
	
	
}

임시로만든 코드를 보자 Member2클래스에 Team2 클래스를 불러오도록 메서드 선언 해주고,

public static void main(String[] args) {
		EntityManagerFactory emf 
			= Persistence.createEntityManagerFactory("hello");
		EntityManager em = emf.createEntityManager();
		
		EntityTransaction tx = em.getTransaction();
		tx.begin();
		
		// 수정
		Team2 team2 = new Team2();
		team2.setName("TeamB");
		em.persist(team2);
		
		Member2 member2 = new Member2();
		member2.setUsername("member2");
		member2.setTeam(team2); // 연관관계 주인에게 값 설정
		em.persist(member2);
		
		
		
		tx.commit();
		em.close();
		emf.close();
	}

위와 같이 member2에서 setTeam메서드를 불러와서 team2객체 값을 넣어주면 된다.
team2 객체에 추가한 값을 그냥 Member2에 set메서드를 통해 한번 선언 해주면 업데이트가 되는것이다.

Team클래스 (Owner X) 경우

Team 객체에 값을 넣고, 생성된 값을 Member 클래스에 값을 업데이트하고 그 걸 다시 Team 클래스 즉, 자기 자기자신에게 다시 덮어 씌우는 2중 과정을 거쳐야한다.

	// Member 클래스에 추가한 메서드
    public void setTeam(Team team) {
		this.team = team;
	}
    
	// Team 클래스에 추가한 메서드, 
	public void addMember(Member member) {
		member.setTeam(this);
		this.member.add(member);
	}
	
    // 엔티티매니저에서 실행 구문
	team.addMember(member); // 1의 입장에서 값 넣기

위 코드가 추가로 더 필요하다.

일대다인 시점의 Team 클래스에서는 member클래스를 불러와서 set하고 다시 그 인스턴스를 에 member 객체에 추가하는게 보인다. 서로 체인되는걸 제대로 인지 안한다면 어렵다..

  • 커밋 전 단계에서 member에 저장된 값을 select하려 했더니 Main 클래스에 insert문만 실행되고, select문은 실행되지 않았다.
  • team이 영속성 컨텍스트에 들어가 있는데, 현재는 member가 할당되지 않은 상태에서 team으로 검색을 하니 1차캐시에 담겨있는 내용 그대로가 조회가 된것
  • 즉, 영속성 컨텍스트 영역사용시 team을 중심으로 member의 조회가 되지 않는다.
    해결 방법은 커밋 전, flush를 통해 DB로 날려주고 그 값을 다시 꺼내오는것, flush를 한 뒤에는 clear문을 선언해 주느것도 잊지말자.
  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
    -> 연관관계 편의 메서드를 생성하자
  • 양방향 매핑시 무한 루프를 조심하자.
    -> toString(), lombok 사용 시 조심

6. 연관관계 편의 메서드 생성

사실 위처럼 EntityManager가 들어있는 클래스에 코드를 짜 넣을 필요없이, 각 객체에 메서드를 미리 짜두어 코드를 반복해서 사용할 필요없이 할 수 있다.

이렇게 한다면 코드가 더 깔끔해진다.

  • N(다)에 넣기 : Member에 메서드 추가
	@ManyToOne
	@JoinColumn(name="TEAM_ID")
	@Setter(value=AccessLevel.NONE) // lombok에서 자동 setter를 막는다.
	private Team team;
	
	// 일방적인 setter의 형태가 아니면 매서드 이름을 바꿔준다.
	// 추후 코드르 봤을때 단순 setter 작업이 아닌 중요한 작업을 진행하는지를 파악할 수 있다.
	public void changeTeam(Team team) {
		this.team = team;
		// this : 나 자신의 인스턴스(객체)
		team.getMember().add(this);
		// team.getMember().add(member); 를 여기서 만듬
	}
  • 1(일)에 넣기 : Team에 메서드 추가
 // 다대일, Team에는 여러 Member가 있기에 배열로 나타낸다. 일대다
	// mappedBy = "team"은 Member에있는 Private Team team에 객체이름
	@OneToMany(mappedBy = "team")
	private List<Member> member
		= new ArrayList<>();
	
	public void addMember(Member member) {
		member.setTeam(this);
		this.member.add(member);
	}

7. 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것 뿐
  • 양방향 사용 이유 : JPQL에서 역방향으로 탐색할 일이 많기 때문
  • 단방향 매핑을 잘 하고, 양방향은 필요할 때 추가해도 됨
    (테이블에 영향을 주지 않음)

8. 연관관계의 주인을 정하는 기준

  • 비지니스 로직을 기준으로 연관관계 주인을 선택하면 안됨
  • 연관관계의 주인은 외래키의 위치를 기준으로 정해야함

    즉 외래키가 있는 테이블이 오너이므로 헤깔리지 말자.

startDate와 endDate는 하나의 묶음, city,street,zipcode를 한묶음으로 볼 수 있다.

그렇다면 자바에서 했던 객체지향을 할 수있지않을까? 이러한 접근을 임베디드로 하는것이다.

9. 임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있다.
  • JPA는 임베디드 타입(embedded type)이라고 함
  • 주로 기본 값 타입을 모아서 만들어서 복합값 타입이라고도 함

9-1. JPA에서 임베디드 타입 사용법

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수
	@Embedded
	private Period period;
    @Embedded
	private Address address;
@Embeddable
@Getter @Setter
public class Period {

	private LocalDateTime startDate;
	private LocalDateTime endDate;
	
	// 테스트를 위해 파라미터가 있는 생성자
	public Period(LocalDateTime startDate, LocalDateTime endDate) {
		super();
		this.startDate = startDate;
		this.endDate = endDate;
	}
	
	public Period() {}
}

두번째 코드처럼 하나의 클래스를 만들어 @Embeddable 을 붙여주고 @Embedded를 선언하여 따로 관리가 가능하다. DB테이블에서는 달라진 점이없으나, 개발자시점에서는 관리하기 매우 용이해진다.

9-2. 임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • Period.isWork() 처럼 해당 값 타임만 사용하는 의미있는 메서드를 만들 수 있다. (객체지향적 설계가 가능하다)
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티 생명주기에 의존한다.

9-3. 임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다
  • 객체와 테이블을 아주 세밀하게 매핑하는것이 가능
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

9-4. @AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하려면 컬럼 명이 중복

  • @AttributeOverride, @AttributeOverrides를 사용해서 컬럼명 속성을 재 정의

    자바의 오버라이드 속성을 기억한다면 별 어려움이 없다. 즉 기존에 가지고 있는 변수타입을 크게 해치지 않는 선에서 변수명만 덧씌워준다

9-5. 객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이아 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.

10. 객체지향 쿼리 언어(JPQL)

10-1. JPA는 다양한 쿼리 방법을 지원

  • JPQL
  • QueryDSL
  • JPA Criteria
  • 네이티브 SQL
  • JDBC API 직접 사용
  • MyBatis
  • SpringJdbcTemplate

10-2. JPQL 소개

  • 단순한 조회 : EntityManager.find();
    -> 나이가 18살 이상인 회원을 모두 검색하고 싶다면?

10-3. JPQL이란

Java Persistence Query Language 의 약자, JPA를 사용하면 엔티티 객체를 중심으로 개발하게 되는데. 여기서 걸리는 점이 쿼리 문이다.

  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 모든 DB데이터를 객체로 변환해서 검색하는 것은 불가능
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공
  • SQL문법이 유사, select, from, where, group by, having, join 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리 작성
  • SQL은 데이터 베이스 테이블을 대상으로 쿼리 작성
  • JPQL을 한마디로 정의하면 객체 지향 SQL

10-4. JPQL 문법

  • 대소문자 구분한다.
    -> 엔티티와 속성은 대소문자 구분, JPQL 키워드는 구분하지 않는다.
  • 엔티티 이름
    -> 테이블명 대신 엔티티명을 사용
  • @Entity(name="...") 이러면 class명 대신, name에 지정한 이름이 들어간다. (기본값을 사용하는것을 추천한다.)
  • 별칭은 필수
    -> JPQL은 별칭이 필수
    -> AS 예약어는 생략 가능
profile
메모리폼
post-custom-banner

0개의 댓글