Develop/Springboot

[JPA] 연관관계 매핑 | [JPA] Association Mapping

인프런에서 에서 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 듣고 쓴 정리 글입니다.https://www.inflearn.com/course/ORM-JPA-Basic 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 서버 데이터베이스 프레임워크 및 라이브러리 프로그래밍 언어 서비스 개발 Java JPA 스프링 데이터 JPA 온라인 강의www.inflearn.com평소에 Spring Data JPA 를 썼는데, 김영한님은 JPA 자체를 강의하시더라구요.김영한님 강의 바탕으로 Spring Data ..

[JPA] 연관관계 매핑 | [JPA] Association Mapping

728x90

인프런에서 에서 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 듣고 쓴 정리 글입니다.

https://www.inflearn.com/course/ORM-JPA-Basic

 

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

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 서버 데이터베이스 프레임워크 및 라이브러리 프로그래밍 언어 서비스 개발 Java JPA 스프링 데이터 JPA 온라인 강의

www.inflearn.com

평소에 Spring Data JPA 를 썼는데, 김영한님은 JPA 자체를 강의하시더라구요.

김영한님 강의 바탕으로 Spring Data JPA로 강의 소스를 테스트해보고 개념을 기록하기 위해 포스팅을 하게되었습니다.


 


연관관계 매핑 기초

1. 단방향 연관관계

<아직 안들음>

2. 양방향 연관관계와 연관관계의 주인 : 기본

양방향 연관관계 -> 양쪽으로 참조한다.

객체 : 참조를 활용
테이블 : FK를 이용한 join

객체-테이블 사이 패러다임 차이를 봐야한다.

2-1. 테이블 연관관계

단방향과 양방향과 차이가 없다.

TEAM->MEMBER 알고싶든, MEMBER -> TEAM알고싶든 Foreign Key로 join해서 알 수 있다.
양방향 단방향 상관없이 FK로 모든 연관관계 알 수 있다.

2-2. 객체 연관관계

Member에서 Team변수를 갖고있으면 Team으로 갈 수 있다.
Team에서는 List를 갖고있어야 Member로 갈 수 있다.

 

멤버변수로 다른 객체를 갖고있어야 서로에게 접근이 가능하다.

 

[참고] : List 멤버변수를 사용할 땐 꼭 new ArrayList<>() 이용해서 초기화를 해주자!
add() 할 때 NullPointError가 안뜨게!

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    @Builder
    private Team(String name) { //여기 그냥 members도 param으로 넣었다가 에러 팡!
        this.name = name;
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name= "TEAM_ID")
    private Team team;

    @Builder
    private Member(String username, Team team) {
        this.username = username;
        this.team = team;
    }
}

궁금한 것

EntitiyTransaction tx = em.getTrasaction();
em.persist(team);
em.flush();
em.clear();

반대 방향으로도 그래프 탐색이 가능해 진다.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Before
    public void setUp() throws Exception {
        Team team = Team.builder()
                .name("TeamA")
                .build();

//        teamRepository.save(team);

        Member member = Member.builder()
                .username("member1")
                .team(team)
                .build();

        memberRepository.save(member);
    }

    @Test
    public void 잘_저장되었는지_불러오기() {
        Member member = memberRepository.findAll().get(0);
        String username = member.getUsername();
        assertThat(username).isEqualTo("member1");

        Team team = member.getTeam();
        assertThat(team.getName()).isEqualTo("TeamA");

        List<Member> members = team.getMembers();
        for (Member m : members) {
            assertThat(m.getUsername()).startsWith("member");
        }

    }

강좌랑 cascade 부분만 달라서 왜 그런가 하고 생각해 봤는데 강좌에서는 save를 두번 했었다.
강좌코드 대로 코딩하고 테스트한 결과는 아래

@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

//    @ManyToOne(cascade = CascadeType.ALL)
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @Builder
    private Member(String username, Team team) {
        this.username = username;
        this.team = team;
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    @Builder
    private Team(String name) { //여기 그냥 members도 param으로 넣었다가 에러 팡!
        this.name = name;
    }
}

테스트코드

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Before
    public void setUp() throws Exception {
        Team team = Team.builder()
                .name("TeamA")
                .build();

        teamRepository.save(team);

        Member member = Member.builder()
                .username("member1")
                .team(team)
                .build();

        memberRepository.save(member);
    }

    @Test
    public void 잘_저장되었는지_불러오기() {
        Member member = memberRepository.findAll().get(0);
        String username = member.getUsername();
        assertThat(username).isEqualTo("member1");

        Team team = member.getTeam();
        assertThat(team.getName()).isEqualTo("TeamA");

        List<Member> members = team.getMembers();
        for (Member m : members) {
            assertThat(m.getUsername()).startsWith("member");
        }

    }
}

Member 저장할 때 Team을 저장하도록 cascade 설정을 하지 않고,

TestCode 작성시에 Member save , Team save를 각각 해줬다

Team이 이미 save가 된 상태에서 Member를 save할 경우인데,

어차피 DB에서는 Member에 FK가 있기 때문에 매핑이 가능해진다!

객체에서의 매핑은 이미 Team, Member 모두 각자의 참조객체를 갖고있기 때문에 가능하고!

내가 처음에 작성한 코드의 경우에는 member만 저장해서 team도 같이 저장하는 것이었기 때문에 Member를 저장할 때 cascade 옵션을 줘야했다.

따라서 Member repository에 저장하더라도, Team의 Insert를 먼저 실행 후에, Member insert를 진행하여 Member Table의 FK에 Team을 저장해준다.

Hibernate: insert into team (member_id, name) values (null, ?)
Hibernate: insert into member (member_id, team_id, username) values (null, ?, ?)

Hibernate: select member0_.member_id as member_i1_0_, member0_.team_id as team_id3_0_, member0_.username as username2_0_ from member member0_

Q. 양방향 매핑이 좋은가?

A. 객체는 사실 단방향이 좋다! -> 신경쓸게 많음

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

2-3-1. 객체의 연관관계 - 2개

​ Member -> Team 연관관계 1개 (단방향) - Team 레퍼런스 객체

​ Team -> Member 연관관계 1개 (단방향) - Member 레퍼런스 객체

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다
  • 객체를 양방향으로 참조하려면 단방향 연관 관계를 2개 만들어야 한다.
class Member{
    Team team;    // TEAM -> Member (team.getMember())
}
class Team{
    Member member;    // MEMBER -> TEAM (member.getTeam())
}

2-3-2. 테이블의 연관관계 - 1개

​ Team <-> Member 연관관계 1개 (양방향) - FK하나로 양쪽의 연관관계 알 수 있음 (join)

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 조인할 수 있다.)
SELECT * 
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELECT * 
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

2-4. 연관관계의 주인

딜레마가 생긴다 > solution : 둘중 하나로 외래키를 관리한다!

  • Team에 있는 List 로 FK를 관리할지
  • Member에 있는 Team으로 FK를 관리할지

2-4-1. 양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리 (등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

mappedBy : 나는 누군가에 의해서 매핑이 되었어! 나는 주인이 아니야!

public class Team {
    @OneToMany(mappedBy = "team") 
    private List<Member> members = new ArrayList<>();
}

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

mappedBy : 나는 team에 의해서 관리가 된다 : Member 객체의 team 변수에 의해서 관리된다.

@JoinColumn의 Team: 나는 앞으로 Team을 관리할꺼야

2-4-2. 누구를 주인으로?

  • 외래키가 있는 곳을 주인으로 정해라
  • 여기서는 Member.team이 연관관계의 주인!

성능 이슈!

Member의 경우에는 insert 쿼리 하나인데

Team의 경우에는 insert 쿼리 + update 쿼리

DB 입장에서 외래키가 있는 곳이 무조건 N

= N 이 있는 곳이 무조건 주인

= @ManyToOne 이 무조건 주인

3. 양방향 연관관계와 연관관계의 주인 : 주의점, 정리

3-1. 양방향 매핑시 가장 많이 하는 실수

  • 연관관계의 주인에 값을 입력하지 않음
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    public class FailTest {
        @Autowired
        MemberRepository memberRepository;
    
        @Autowired
        TeamRepository teamRepository;
    
        @Autowired
        EntityManager entityManager;
    
        @Test
        public void 일차캐싱에_따른_저장_테스트() {
    
            Team team = Team.builder()
                    .name("TeamA")
                    .build();
    
            teamRepository.save(team);
    
            Member member = Member.builder()
                    .username("member1")
                    .team(team)
                    .build();
    
    //        team.getMembers().add(member);
    
            memberRepository.save(member);
    
            // 주인(Member)이 연관관계를 설정하지 않음!!
            // 역방향(주인이 아닌 방향)만 연관관계 설정
    //        entityManager.clear();
    
            Team findTeam = teamRepository.findAll().get(0);
            List<Member> members = findTeam.getMembers();
    
            assertThat(members).isEmpty();
        }
    }
  • entityManager.clear(); 을 안했을 경우
    : 1차 캐시를 해서 영속성 컨텍스트가 되어있는 상태 값 세팅 연관관계가 되어있는걸 그냥 가져온다.
    이렇게 실행하면 DB에서 select 쿼리가 안 나간다.
  • Team이 그냥 영속성 컨텍스트에 들어가있어서, team에는 현재 member가 없는상태.
    그러다보니 1차 캐싱으로 인해 아무것도 안들어가 있음!
  • 객체지향적으로 양쪽다 값을 입력해야 한다!

3-2. 양방향 연관관계 주의

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 성정하자
  • 연관관계 편의 메소드를 생성하자
  • 양방향 매핑시에 무한루프를 조심하자
    예 ) toString(), lombok, JSON 생성 라이브러리
Team team = Team.builder()
    .name("TeamA")
    .build();

teamRepository.save(team);

Member member = Member.builder()
    .username("member1")
    .team(team)
    .build();

team.getMembers().add(member);

이런식으로 Member에 한줄을 넣어주기 보다! 연관관계 편의 메소드를 생성하자

 

Member에서 team을 set 해줄때 설정해버린다. - 하나면 세팅해도 두개가 같이 세팅이 되게!

@Builder
private Member(String username, Team team) {
    this.username = username;
    this.team = team;
    team.getMembers().add(this);
}

편의 메소드는 일에 넣어도 되고, 다에 넣어도 된다 : 상황을 보고 만들기를 추천한다.

 

@ToString / toString() 메소드

//Team 클래스
@Override
public String toString() {
    return "Team{" +
        "id=" + id +
        ", name='" + name + '\'' +
        ", members=" + members +
        '}';
}

//Member 클래스
@Override
public String toString() {
    return "Member{" +
        "id=" + id +
        ", username='" + username + '\'' +
        ", team=" + team +
        '}';
}

JSON 생성 라이브러리 : entity를 바로 Controller에서 바로 response 해버릴때 문제가 생긴다.

Member > Team > Member > Team > Member > Team > ...

  1. lombok에서 toString을 쓰지마라
  2. Controller에는 절대 Entity를 반환하지 마라.

4. 정리

4-1. 양방향 매핑 정리

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

JPA에서의 설계는 단방향만으로도 객체와 테이블의 매핑이 완료되어야한다.

테이블은 한번 만들면 굳어지는 것!

4-2. 연관관계의 주인을 정하는 기준

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

This is a summary post based on Kim Young-han's Java ORM Standard JPA Programming - Basics course on Inflearn.

https://www.inflearn.com/course/ORM-JPA-Basic

 

Java ORM Standard JPA Programming - Basics - Inflearn

For those who are new to JPA or use JPA in practice but lack foundational theory — this course helps you build a solid understanding of JPA basics so that even beginners can confidently use JPA in real-world projects. Beginner Web Development Server Database Frameworks & Libraries Programming Languages Service Development Java JPA Spring Data JPA Online Course

www.inflearn.com

I've been using Spring Data JPA, but Kim Young-han actually teaches JPA itself.

Based on his lectures, I decided to write this post to test the course material using Spring Data JPA and document the concepts.


 


Association Mapping Basics

1. Unidirectional Association

<Haven't watched this part yet>

2. Bidirectional Association and the Owner of the Relationship: Basics

Bidirectional association -> references go both ways.

Object: uses references
Table: uses FK joins

We need to look at the paradigm difference between objects and tables.

2-1. Table Associations

There's no difference between unidirectional and bidirectional.

Whether you want to know TEAM->MEMBER or MEMBER->TEAM, you can find out by joining with the Foreign Key.
Regardless of whether it's bidirectional or unidirectional, you can figure out all associations with just the FK.

2-2. Object Associations

If Member has a Team variable, it can navigate to Team.
Team needs to have a List to navigate to Member.

 

Objects need to hold references to each other as member variables to be able to access one another.

 

[Note]: When using a List member variable, always initialize it with new ArrayList<>()!
This prevents NullPointerError when calling add()!

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    @Builder
    private Team(String name) { //여기 그냥 members도 param으로 넣었다가 에러 팡!
        this.name = name;
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name= "TEAM_ID")
    private Team team;

    @Builder
    private Member(String username, Team team) {
        this.username = username;
        this.team = team;
    }
}

Something I was curious about

EntitiyTransaction tx = em.getTrasaction();
em.persist(team);
em.flush();
em.clear();

Now graph traversal is possible in the reverse direction as well.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Before
    public void setUp() throws Exception {
        Team team = Team.builder()
                .name("TeamA")
                .build();

//        teamRepository.save(team);

        Member member = Member.builder()
                .username("member1")
                .team(team)
                .build();

        memberRepository.save(member);
    }

    @Test
    public void 잘_저장되었는지_불러오기() {
        Member member = memberRepository.findAll().get(0);
        String username = member.getUsername();
        assertThat(username).isEqualTo("member1");

        Team team = member.getTeam();
        assertThat(team.getName()).isEqualTo("TeamA");

        List<Member> members = team.getMembers();
        for (Member m : members) {
            assertThat(m.getUsername()).startsWith("member");
        }

    }

The only difference from the course was the cascade part, and when I thought about why, it's because the course called save twice.
Here's the test result after coding it exactly like the course:

@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

//    @ManyToOne(cascade = CascadeType.ALL)
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @Builder
    private Member(String username, Team team) {
        this.username = username;
        this.team = team;
    }
}
@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    @Builder
    private Team(String name) { //여기 그냥 members도 param으로 넣었다가 에러 팡!
        this.name = name;
    }
}

Test code

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberTest {
    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Before
    public void setUp() throws Exception {
        Team team = Team.builder()
                .name("TeamA")
                .build();

        teamRepository.save(team);

        Member member = Member.builder()
                .username("member1")
                .team(team)
                .build();

        memberRepository.save(member);
    }

    @Test
    public void 잘_저장되었는지_불러오기() {
        Member member = memberRepository.findAll().get(0);
        String username = member.getUsername();
        assertThat(username).isEqualTo("member1");

        Team team = member.getTeam();
        assertThat(team.getName()).isEqualTo("TeamA");

        List<Member> members = team.getMembers();
        for (Member m : members) {
            assertThat(m.getUsername()).startsWith("member");
        }

    }
}

Without setting cascade on Member to also save Team,

I called save separately for both Member and Team in the test code.

Since Team is already saved before saving Member,

the mapping works because the FK exists in the Member table in the DB!

On the object side, the mapping works because both Team and Member already hold their own reference objects!

In my original code, I was saving only member and having team saved along with it, so I needed the cascade option on Member.

Therefore, even though we're saving through the Member repository, it executes the Team's INSERT first, then proceeds with the Member INSERT, storing the Team in the Member table's FK.

Hibernate: insert into team (member_id, name) values (null, ?)
Hibernate: insert into member (member_id, team_id, username) values (null, ?, ?)

Hibernate: select member0_.member_id as member_i1_0_, member0_.team_id as team_id3_0_, member0_.username as username2_0_ from member member0_

Q. Is bidirectional mapping better?

A. Actually, unidirectional is better for objects! -> There's a lot more to worry about

2-3. Differences in How Objects and Tables Form Relationships

2-3-1. Object Associations - 2

​ Member -> Team: 1 association (unidirectional) - Team reference object

​ Team -> Member: 1 association (unidirectional) - Member reference object

  • A bidirectional relationship in objects is not truly bidirectional — it's actually two separate unidirectional relationships
  • To reference objects bidirectionally, you need to create two unidirectional associations.
class Member{
    Team team;    // TEAM -> Member (team.getMember())
}
class Team{
    Member member;    // MEMBER -> TEAM (member.getTeam())
}

2-3-2. Table Associations - 1

​ Team <-> Member: 1 association (bidirectional) - A single FK lets you know both sides of the relationship (join)

  • Tables manage the association between two tables with a single foreign key
  • The MEMBER.TEAM_ID foreign key alone provides a bidirectional association (you can join from either side.)
SELECT * 
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELECT * 
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

2-4. The Owner of the Relationship

A dilemma arises > solution: manage the foreign key from one side!

  • Should we manage the FK through the List in Team?
  • Or through the Team in Member?

2-4-1. Bidirectional Mapping Rules

  • Designate one of the two object relationships as the owner of the relationship
  • Only the owner of the relationship can manage the foreign key (create, update)
  • The non-owner side can only read
  • The owner does NOT use the mappedBy attribute
  • The non-owner uses mappedBy to specify the owner

mappedBy: "I'm mapped by someone else! I'm not the owner!"

public class Team {
    @OneToMany(mappedBy = "team") 
    private List<Member> members = new ArrayList<>();
}

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

mappedBy: "I'm managed by team" — meaning it's managed by the team variable in the Member object.

@JoinColumn's Team: "I'm going to manage Team from now on"

2-4-2. Who Should Be the Owner?

  • Make the side where the foreign key exists the owner
  • In this case, Member.team is the owner of the relationship!

Performance issue!

For Member, it's just one INSERT query,

but for Team, it's an INSERT query + an UPDATE query

From the DB's perspective, the side with the foreign key is always the N (many) side

= The N side is always the owner

= @ManyToOne is always the owner

3. Bidirectional Association and the Owner of the Relationship: Pitfalls and Summary

3-1. The Most Common Mistake in Bidirectional Mapping

  • Not setting a value on the owner of the relationship
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    public class FailTest {
        @Autowired
        MemberRepository memberRepository;
    
        @Autowired
        TeamRepository teamRepository;
    
        @Autowired
        EntityManager entityManager;
    
        @Test
        public void 일차캐싱에_따른_저장_테스트() {
    
            Team team = Team.builder()
                    .name("TeamA")
                    .build();
    
            teamRepository.save(team);
    
            Member member = Member.builder()
                    .username("member1")
                    .team(team)
                    .build();
    
    //        team.getMembers().add(member);
    
            memberRepository.save(member);
    
            // 주인(Member)이 연관관계를 설정하지 않음!!
            // 역방향(주인이 아닌 방향)만 연관관계 설정
    //        entityManager.clear();
    
            Team findTeam = teamRepository.findAll().get(0);
            List<Member> members = findTeam.getMembers();
    
            assertThat(members).isEmpty();
        }
    }
  • When entityManager.clear() is not called:
    Since first-level caching is active and the persistence context still holds the values, it just fetches the existing association state.
    When executed this way, no SELECT query is actually sent to the DB.
  • Team is just sitting in the persistence context, and at this point, it has no members.
    Because of first-level caching, nothing is populated!
  • From an object-oriented perspective, you should set values on both sides!

3-2. Bidirectional Association Precautions

  • Always set values on both sides, considering the pure object state
  • Create convenience methods for managing associations
  • Watch out for infinite loops in bidirectional mapping
    e.g.) toString(), Lombok, JSON serialization libraries
Team team = Team.builder()
    .name("TeamA")
    .build();

teamRepository.save(team);

Member member = Member.builder()
    .username("member1")
    .team(team)
    .build();

team.getMembers().add(member);

Rather than adding a separate line for Member like this, create a convenience method for the association!

 

Set it up when setting the team on Member — so that setting one side automatically sets both!

@Builder
private Member(String username, Team team) {
    this.username = username;
    this.team = team;
    team.getMembers().add(this);
}

The convenience method can go on either the One side or the Many side — I recommend deciding based on the situation.

 

@ToString / toString() method

//Team 클래스
@Override
public String toString() {
    return "Team{" +
        "id=" + id +
        ", name='" + name + '\'' +
        ", members=" + members +
        '}';
}

//Member 클래스
@Override
public String toString() {
    return "Member{" +
        "id=" + id +
        ", username='" + username + '\'' +
        ", team=" + team +
        '}';
}

JSON serialization libraries: Problems occur when you return an entity directly from the Controller as a response.

Member > Team > Member > Team > Member > Team > ...

  1. Don't use toString from Lombok
  2. Never return an Entity directly from a Controller.

4. Summary

4-1. Bidirectional Mapping Summary

  • Unidirectional mapping alone already completes the association mapping
  • Bidirectional mapping simply adds the ability to query in the reverse direction (object graph traversal)
  • In JPQL, you often need to traverse in the reverse direction
  • Do unidirectional mapping well first, then add bidirectional when needed (it doesn't affect the table)

In JPA design, object-to-table mapping should be complete with just unidirectional associations.

Once a table is created, it's set in stone!

4-2. Criteria for Choosing the Owner of the Relationship

  • Don't choose the relationship owner based on business logic
  • The relationship owner should be determined based on where the foreign key is located

댓글

Comments

Develop/Springboot

[inflearn] 스프링 부트 개념과 활용 2.스프링 부트 시작하기 | [inflearn] Spring Boot Concepts and Utilization 2. Getting Started with Spring Boot

1. Spring Boot 소개1-1. Spring Boot Start특징토이를 만드는게 아니라 제품수준의 어플리케이션을 만들때 도와주는 툴.opinated view : 스프링 부트가 갖고있는 컨벤션을 의미한다 (널리 사용되는 설정)Spring platform에 대한 기본 설정 뿐만아니라 다른 library에 대한 설정(tomcat)도 기본적으로 해준다목표모든 스프링 개발을 할 때 더 빠르고 더 폭넓은 사용성을 제공한다.일일히 설정하지 않아도 convention으로 정해져있는 설정을 제공한다. 하지만 우리의 요구사항에 맞게 이런 설정을 쉽고 빠르게 바꿀 수 있다.(스프링 부트를 사용하는 이유)non-fucntional 설정도 제공해 준다. 비즈니스로직 구현에 필요한 기능 외에도 non-functional..

[inflearn] 스프링 부트 개념과 활용 2.스프링 부트 시작하기 | [inflearn] Spring Boot Concepts and Utilization 2. Getting Started with Spring Boot

728x90

1. Spring Boot 소개

1-1. Spring Boot Start

특징

토이를 만드는게 아니라 제품수준의 어플리케이션을 만들때 도와주는 툴.

opinated view : 스프링 부트가 갖고있는 컨벤션을 의미한다 (널리 사용되는 설정)

Spring platform에 대한 기본 설정 뿐만아니라 다른 library에 대한 설정(tomcat)도 기본적으로 해준다

목표

  • 모든 스프링 개발을 할 때 더 빠르고 더 폭넓은 사용성을 제공한다.
  • 일일히 설정하지 않아도 convention으로 정해져있는 설정을 제공한다. 하지만 우리의 요구사항에 맞게 이런 설정을 쉽고 빠르게 바꿀 수 있다.(스프링 부트를 사용하는 이유)
  • non-fucntional 설정도 제공해 준다. 비즈니스로직 구현에 필요한 기능 외에도 non-functional feature도!
  • XML 사용하지 않고, code generation도 하지 않는다.

Spring 루 : 독특하게 code generation을 해주는데 지금은 잘 사용되지 않는다. generation을 안해서 더 쉽고 명확하고 커스터마이징하기 쉽다. > spring boot의 bb

System Requirements

Spring boot 는 java 8 이상을 필요로 한다.

지원하는 servletContainer로는 tomcat, jetty Undertow가 있다.

 

2. Spring Boot 시작하기

Intellij ultimate를 사용하면 Spring boot initializer가 있으나, community 버전은 없다. 따라서 자신이 원하는 build tool을 이용해서 만들어 주면된다 Spring boot initializer를 이용하지 않고, 프로젝트 생성하는 법 을 공부 할 것이다.

 

2-1. gradle project에서 시작

 

auto import OK (build.gradle 파일 변경할 때 마다 바로바로 변경 : dependency 추가 등)

spring.io > project > spring boot > Learn > Reference Doc > Gradle Installation

https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/#getting-started-gradle-installation

 

Spring Boot Reference Guide

This section dives into the details of Spring Boot. Here you can learn about the key features that you may want to use and customize. If you have not already done so, you might want to read the "Part II, “Getting Started”" and "Part III, “Using Spring Boot

docs.spring.io

 

build.gradle

build.gradle 파일을 입력해준다.

plugins {
    id 'org.springframework.boot' version '2.0.3.RELEASE'
    id 'java'
}

의존성 관리와 매우 관련이 있는 설정이다.

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

일반적으로 프로젝트는 하나이상의 "starter"에 대한 의존성을 선언한다.
spring boot는 의존성 선언을 간소화했으며, jar를 생성하는데 유용한 Gradle 플러그인을 제공한다.

 

initial build.gradle 파일

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.4.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'spring-boot-getting-started'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

 

SpringBootApplication.java

package com.jyami;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String args[]){
        SpringApplication.run(Application.class, args);
    }

}

 

SpringBootApplication 어노테이션을 사용해서, SpringApplication을 run하는 메소드 호출

Spring MVC가 돌아가려면 여러 dependency가 필요한데, 어떻게 하여 수많은 의존성들이 들어왔는가?

mvc앱을 설정해야하는데 (bean, tomcat .. )

: 이게 @SpringBootApplication에 설정되어있다. > InableAutoCompletecation

 

Intellij 설정에서
Build, Execution, Deployment > Compiler > Annotation Processors에 들어가
Enable annotation processing에 체크해줘야 gradle로 build한 annotation들을 사용할 수 있다.

 

Run (실행하기)

Run을 하고 log를 보면 벌서 Tomcat이 8080 port에서 실행되고 있음을 알 수 있고
http://localhost:8080 을 띄어보면, tomcat web application이 동작함을 알 수 있다. (error이긴 하지만)

 

build (빌드하기)

gradle build

이 package를 build한다. java프로젝트이므로 jar파일이 생성되고, 이 jar파일을 생성한다

java -jar build/libs/spring-boot-getting-started-0.1.0.jar

jar 파일을 실행하면, 아까와 같은 spring web application이 동작하게 된다.

 

 

2-2. 웹으로 Spring Boot project 시작

http://start.spring.io

원하는 build 형태의 spring boot project를 생성해준다. (dir 형태로!)

 

 

3. 스프링 프로젝트의 구조

gradle java 기본 프로젝트 구조와 동일하다

저장 파일 파일 경로 설명
소스 코드  src/main/java -
소스 리소스 src/main/resource java application에서 resources 기준으로 아래 것들을 참조 가능 (classpath)
테스트 코드 src/test/java -
테스트 리소스 src/test/resource test 관련 리소스를 만들 수 있다

 

메인 애플리케이션 위치 (@SpringBootApplication) : 기본 패키지  package com.jyami
프로젝트가 쓰고있는 가장 최상위 패키지! > why? 컴포넌트 스캔을 하기 때문

com.jyami에서부터 시작을 해서, 그 아래에 있는 파일들을 스캔해서 bean으로 등록한다.

 

src/main/java 위치에 넣으면 모든 패키지를 스캔하므로

 

만약 java>com.hello 패키지가 있고, 그안에 메인 애플리케이션이 아닌 java파일이 있으면, 그 java파일은 component 스캔이 이루어지지 않는다.

1. Introduction to Spring Boot

1-1. Spring Boot Start

Features

It's a tool that helps you build production-level applications, not just toy projects.

opinated view : This refers to the conventions that Spring Boot has (widely used configurations)

It provides default configurations not only for the Spring platform but also for other libraries (like tomcat) out of the box.

Goals

  • Provides faster and broader usability for all Spring development.
  • Provides convention-based configurations without having to set everything up manually. But you can easily and quickly change these settings to match your requirements. (This is the reason to use Spring Boot)
  • Provides non-functional configurations as well. Beyond features needed for business logic, it also covers non-functional features!
  • Does not use XML and does not do code generation.

Spring Roo : It uniquely does code generation, but it's not widely used anymore. By not doing generation, things are easier, clearer, and simpler to customize. > The predecessor of Spring Boot

System Requirements

Spring Boot requires Java 8 or higher.

Supported servlet containers include Tomcat, Jetty, and Undertow.

 

2. Getting Started with Spring Boot

If you use IntelliJ Ultimate, it has a Spring Boot Initializer built in, but the Community edition does not. So you can create one using whichever build tool you prefer. We'll learn how to create a project without using the Spring Boot Initializer.

 

2-1. Starting from a Gradle Project

 

auto import OK (Applies changes immediately whenever the build.gradle file is modified: adding dependencies, etc.)

spring.io > project > spring boot > Learn > Reference Doc > Gradle Installation

https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/#getting-started-gradle-installation

 

Spring Boot Reference Guide

This section dives into the details of Spring Boot. Here you can learn about the key features that you may want to use and customize. If you have not already done so, you might want to read the "Part II, “Getting Started”" and "Part III, “Using Spring Boot

docs.spring.io

 

build.gradle

Enter the build.gradle file content.

plugins {
    id 'org.springframework.boot' version '2.0.3.RELEASE'
    id 'java'
}

This is a configuration closely related to dependency management.

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Typically, a project declares dependencies on one or more "starters".
Spring Boot simplifies dependency declarations and provides a useful Gradle plugin for generating jars.

 

initial build.gradle file

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.4.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'spring-boot-getting-started'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

 

SpringBootApplication.java

package com.jyami;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String args[]){
        SpringApplication.run(Application.class, args);
    }

}

 

Using the SpringBootApplication annotation to call the method that runs SpringApplication

Spring MVC requires many dependencies to run — so how did all those dependencies get pulled in?

We need to configure the MVC app (bean, tomcat, etc.)

: This is all configured in @SpringBootApplication. > EnableAutoConfiguration

 

In IntelliJ settings, go to
Build, Execution, Deployment > Compiler > Annotation Processors and
check Enable annotation processing to be able to use annotations built with Gradle.

 

Run

When you run the application and check the logs, you can see that Tomcat is already running on port 8080.
If you open http://localhost:8080, you can confirm that the Tomcat web application is working. (Even though it shows an error page)

 

Build

gradle build

This builds the package. Since it's a Java project, a jar file is generated.

java -jar build/libs/spring-boot-getting-started-0.1.0.jar

When you run the jar file, the same Spring web application will start up just like before.

 

 

2-2. Starting a Spring Boot Project from the Web

http://start.spring.io

It generates a Spring Boot project in your desired build format. (As a directory structure!)

 

 

3. Spring Project Structure

It follows the same structure as a standard Gradle Java project.

File Type File Path Description
Source Code  src/main/java -
Source Resources src/main/resource In a Java application, files below the resources directory can be referenced (classpath)
Test Code src/test/java -
Test Resources src/test/resource You can create test-related resources here

 

Main Application Location (@SpringBootApplication) : Default package  package com.jyami
This should be in the topmost package of the project! > Why? Because of component scanning.

Starting from com.jyami, it scans files underneath and registers them as beans.

 

If you place it directly under src/main/java, it would scan all packages.

 

If there's a package java>com.hello with a Java file that isn't the main application, that Java file will not be picked up by component scanning.

댓글

Comments