Backend/개발

[Spring Boot] mysql 연동 , DB 연관관계 고민

지수쓰 2021. 8. 4. 01:24
반응형

TIL35일차 개인프로젝트 기록 

mysql

휘발성인 h2가 아닌 mysql DB에 데이터를 저장하고 로직을 구현해야할 것 같아서 mysql 연동 설정을 해주었다.

 

mysql dependency 추가

% mysql --version
mysql  Ver 8.0.23 for osx10.15 on x86_64 (Homebrew)
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.23'

 


application.yml 설정 관련

Datasource1

서로 다른 Database connection pool 구현체 연동을 위해 만든 표준 인터페이스

DBCP : Database connection pool

DBCP 기본 option : maxActive, maxIdle, minIdle, initialSize

 

Spring boot 2.4 이후 spring.config.activate.on-profile

하나의 application.yml 파일에 여러 환경의 설정 정보를 저장할때 profile를 사용하고, ---로 구분한다.

그리고 java -jar myapp.jar --spring.profiles.active=prod 이런식이나

인텔리제이에서 Edit Configuration에서 Active profile값을 지정해주면 그 프로파일로 실행할 수 있다.

나는 주석처리하고 다시 돌리고,,이랬는데 테스트 DB나 환경이 다를때 프로파일로 구분하면 되는 것 같다.

 

spring.output.ansi.enabled: ALWAYS

콘솔에서 뜨는 로그에 색이 입혀진다.

 

hibernate show_sql, format_sql , ddl

spring:
  jpa:
    properties:
      hibernate:
          ddl-auto: update # 변경된 스키마 적용 -> 배포용 파일엔 부적합 
        show_sql: true # 콘솔에 쿼리를 보여줌
        format_sql: true # 보기좋게 포맷팅해줌 ( 줄바꿈 , 탭 )
    generate-ddl: true # @Entity가 명시된 클래스를 찾아 ddl 생성하고 실행

 

logging.level.org.hibernate.type.descriptor.sql

logging:
  level:
    org:
      hibernate:
        type:
          descriptor:
            sql: trace

바인드되는 파라미터값이 출력된다. insert value(?,?,?)에 뭐가들어가는지 등

 


결과

프로젝트 처음 실행시

# generate-ddl : true, hibernate.ddl-auto:update라서 생성되었고
# show_sql이라서 로그를 볼 수 있고 
# format_sql이라서 줄바꿈되어 이쁘게 출력된다
Hibernate: 

    create table participants (
       id bigint not null auto_increment,
        created_date datetime(6),
        modified_date datetime(6),
        comment varchar(255),
        current_tier varchar(15) not null,
        highest_tier varchar(15) not null,
        main_position varchar(255) not null,
        point bigint,
        sub_positions varchar(255),
        summoner_name varchar(255) not null,
        primary key (id)
    ) engine=InnoDB

 

값 추가시

Hibernate: 
    insert 
    into
        participants
        (created_date, modified_date, comment, current_tier, highest_tier, main_position, point, sub_positions, summoner_name) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?)

values에 무슨 값을 입력했는지 볼 수 있다.

 

35-2

35명의 참가자를 DB에 등록했다.

아무래도 등록하는과정이 굳이 번거로울 필요는 없는 것 같고,, 등록 구현한건 연습한거 정도로 치고 나중에는 디비에 그냥 입력하는것도 생각해봐야겠다.

 

추가로 comment에 값이 안들어가서 봤더니 html에 name="comment"값이 빠져있어서 넣어주었다.


Team - Participant 관계 설정

김영한님 JPA강좌를 참고해서 쓴 블로그들이 많고, 거기서 team-member관계를 예로 들어서 참고하기 좋았다.

참고한 블로그들 - 두고두고 읽어두면 좋을 것 같다.

//Teams
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name ="TEAM_ID")
private Long id;

@OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
private List<ParticipantsEntity> participants = new ArrayList<>();
//Participants
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private TeamsEntity team;

객체의 두 관계중 하나의 연관관계를 주인으로 지정해 연관관계의 주인 만이 외래키를 관리(등록, 수정) 한다.

주인이 아닌 쪽은 읽기만 가능하고 mappedBy속성으로 주인을 지정하며, 주인은 mappedBy 속성을 지정하지 않는다.

mappedBy : 나는 By에 의해 매핑되었다 라는 의미이다

@OneToMany(mappedBy=team) : Participants 객체의 team 변수에 의해서 관리가 된다.

@JoinColumn(name=Team_id) : team을 관리할 것이다.

외래키가 있는 곳을 주인으로 정하기 때문에 Participants.team이 연관관계의 주인이다.

이때 성능이슈가 발생할 수 있다.

  • Participants의 경우 insert쿼리만 하면 되지만 Team의 경우 insert+update 가 둘다 일어난다

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

(외래키가 있는 곳 = N이 있는 곳 = 주인 = @ManyToOne)

 

주의해야할 점

연관관계의 주인에 값을 입력하지 않는 실수

toString, lombok, json 생성시 주의

 

그리고 Member와 team을 조회할때 member를 가져올 때 member만 가져올지, team을 가져올지 고민해야한다.

테이블 생성 결과

Hibernate: 

    alter table participants 
       add column team_id bigint
Hibernate: 

    create table teams (
       team_id bigint not null auto_increment,
        leader_id bigint,
        points bigint,
        team_name varchar(255),
        primary key (team_id)
    ) engine=InnoDB
Hibernate: 

    alter table participants 
       add constraint FKd9fj9qgl0laglppk9oxdrjwkp 
       foreign key (team_id) 
       references teams (team_id)

35-3

테이블을 생성했다.

 


Team-participants 연결 테스트

테스트 코드
    @AfterEach
    public void cleanAll() {
        teamsRepository.deleteAll();
    }

    @BeforeEach
    public void setup() {

        log.info("setup");
        List<TeamsEntity> teams = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            TeamsEntity teamsEntity = TeamsEntity.builder()
                    .teamName(i + "팀")
                    .build();
            teamsEntity.addParticipants(ParticipantsEntity.builder()
                    .summonerName("감귤or가씨")
                    .currentTier("silver2")
                    .highestTier("silver2")
                    .mainPosition("SUP")
                    .point(200l)
                    .build()
            );
            teams.add(teamsEntity);
            teamsRepository.save(teamsEntity);
        }

    }

    @Test
    public void 팀조회() {
        // given
        List<String> participantsNames = teamsService.findAllPariticipantsNames();

        // then
        assertThat(participantsNames.size()).isEqualTo(5);
    }

 

Error: LazyInitializationException

테스트에서는 새로 생성한다음 넣어주었지만, 실제로는 미리 저장되어있는 participants중에 가져와 미리 생성된 team에 등록해줄 것이라 생각해서 이렇게 코드를 짰었다.

teamsEntity.addParticipants(participantsRepository.getOne((long) i));
could not initialize proxy [com.project.auction.lol.domain.ParticipantsEntity#1] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.project.auction.lol.domain.ParticipantsEntity#1] - no Session
at com.project.auction.lol.domain.ParticipantsEntity$HibernateProxy$19hrRstI.updateTeam(Unknown Source)
    at com.project.auction.lol.domain.TeamsEntity.addParticipants(TeamsEntity.java:49)

LazyInitializationException 오류가 났다.

repository의 get과 findById의 차이가 findById는 바로 가져오는거고, get은 지연로딩인데 get은 프록시로 가져와서 테스트할때 값 지정이 안되서 그러는 것 같다. 다음 코드로 변경해주었다.

teamsEntity.addParticipants(participantsRepository.findById((long)i).get());

 

테스트코드에서는 굳이 이럴 필요가 없는 것 같아서 Participants.builder를 이용해 새로 생성한 값을 넣어주었다.

 

Error: cascade

assertThat(participantsNames.size()).isEqualTo(5); 테스트 코드가 실제는 0 기대는 5로 실패했다.

teamsRepository.save(teamsEntity); 원래는 이 코드에서

Team생성 - team에 participant넣고 save하는데 participants가 저장이 안돼서 team.getparticipants를 해도 불러와지지가 않았는데, cascade를 지정해주어야 하는 것이었다.

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

 

Cascade란

Entity의 사이태 변화를 전파시키는 옵션이다.

Entity에 상태 변화가 있을 때 연관되어 있는 (OneToMany, ManyToOne) Entity에도 상태 변화를 전이시키는 옵션이다. (default : none)

onetomany 관계일때 부모->여러 자식 전파를 위해 one에 cascade 옵션을 넣어주는 것 같다.

 


Fetch, N+1 문제 이 예제와 연관하여 생각해보기 -- 정리 안됨..

위의 테스트 코드가 JPA n+1 예제를 생각하면서 따라해본 예제인데, 로그를 봐도 이해가 잘 되지 않아서 다시 천천히 알아보려고 한다.

 

N+1이라는 말이 나타내는 의미는 다음과 같다.

1: 처음에 team 조회 쿼리

N : team에 연결된 participanst를 가져오기 위한 추가 쿼리

만약에 참가자가 100만이라면? 팀1개조회하는데 N(100만)개의 쿼리가 추가로 불려지는 문제가 생긴다.

 

일반적인 N+1조회

35-3

fetchjoin으로 한 쿼리에 해결

@Query("select t from teams t join fetch t.participants")

35-3

해결 방법 - Lazy, fetchjoin

fetchjoin : runtime에 동적으로 원하는 것만 선택해서 가져올 수 있다.

하지만 한계는 fetch join을 하면 페이징과 둘 이상의 컬렉션을 패치할 수는 없다.

 

그리고 보통 @ManyToOne에는 fetch lazy를 준다고 해서 넣어주었다.(아직 이부분도 이해가 잘 안됨..)

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private TeamsEntity team;

Stream

다음 코드는 team에서 participants중 첫번째( 테스트상 하나만 넣어놔서 그냥 get(0)을 한 상황이다) 를 불러와 summonername을 리턴해주는 코드이다. 예제에서 stream, map, collect를 썼는데 이해가 안돼서 같은 의미로 동작하는 코드를 짜보았는데, 이참에 무슨 의미인지 공부하고 비교를 해보려고 한다.

List<String> result = new ArrayList<>();
for( TeamsEntity entity: teams){
  result.add(entity.getParticipants().get(0).getSummonerName());
}
return result;

return teams.stream()
  .map(t-> t.getParticipants().get(0).getSummonerName())
  .collect(Collectors.toList());
  • map : 변환메소드 / 해당 스트림의 요소를 주어진 함수에 인수로 전달하여 ,그 반환값들로 이루어진 새로운 스트림을 반환한다
  • collect는 stream의 아이템을 원하는 자료형으로 변환할 수 있다.

즉 map으로 teams의 하나의 요소인 teamsEntity를 전달해서, 반환값인 summonerName으로 이루어진 스트림을 만든다 ( String의 stream일것 ) -> 이것을 toList()해서 List<String>으로 만들어준 것이다.

 


정리

mysql 연동 해봤고, Team-participant 연관관계 고려해서 테이블 생성을 해봤다.

아직 n+1 문제 , fetch 로 해결하는 문제는 잘 모르겠다. 더 정리해서 올리도록 하겠담

실제 기능 구현 코드를 슬슬 짜봐야겠다...

출처

스프링 부트 Auto-configuration과 JPA(하이버네이트) SQL문 로깅

JPA-엔티티상태-Cascade

JPA Lazy Evaluation

jojoldu JPA N+1문제 및 해결 방안

workbench로 erd 추출

쟈미님블로그