본문 바로가기
우아한테크코스/Level2

[우아한테크코스] JPA에서 감자로 살아남기🥔

by kriorsen 2024. 5. 18.

📌  새침데기 공주님과의 페어 프로그래밍


"안녕하세요~ 🥰😊😇🌷"
"왜요 👿"

 

페어 위브의 충격적인 첫인사가 아직도 생생하게 떠오른다. 솔직히 저 인사 이후로 혼자 속으로 겁먹고 있었는데, 위브는 나랑 안 맞는다고 투덜거리면서도 페어 중에 자세 불편하지 않냐고 걱정해 주고, 머리 아프다고 하니까 타이레놀도 나눠 주고, 페어 끝나고는 같이 밥도 먹고 뽀로로 약과도 사 줬다. 2레벨에 만난 페어들은 다 반전 매력이 있는 것 같다 ㅎㅎ 아님그냥내가우테코최강쫄보라서그런가 🥲

 

📌 JPA요? 한 입 거리밖에 안 되죠


물론 제가요 🥔

 

애매하게 아는 것은 모르는 것만 못하다.
- Potato Kim

 

TypeORM과 JPA 경험이 없는 것도 아니었으면서 막상 미션 시작하니 또 @ManyToOne과 @OneToMany가 헷갈렸다. 강의 중에 정 모르겠으면 그냥 아무거나 적용해 보고 돌아가는 걸로 쓰라고 했던 브라운의 발언에 빵 터졌었는데, 지금 생각해 보니 내 얘기였네...

 

애플리케이션 테이블 구조

@ManyToOne 

위와 같은 테이블 구조에서 하나의 Member, Theme, Time은 여러 Reservation을 가질 수 있는 다대일 관계가 형성되어 있다. 그래서 이때 Reservation은 Many에 해당되고, 나머지 엔티티들은 One에 해당되어 다음과 같이 연관관계를 정의할 수 있다.

@Entity
public class Reservation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private Member member;

    private ReservationDate date;

    @ManyToOne
    private ReservationTime time;

    @ManyToOne
    private Theme theme;

    private ReservationStatus status;
}

 

@OneToMany

하나의 Member, Theme, Time을 가지는 Reservation과 반대로 Member와 같은 엔티티에서는 여러 Reservation을 가질 수 있으므로 Member가 One, Reservation이 Many에 해당된다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private MemberName name;

    private MemberEmail email;

    private MemberPassword password;

    private MemberRole role;
    
    @OneToMany(mappedBy = "member")
    private List<Reservation> reservations;
}

 

📌 ???: 돌아가는 쓰레기를 만들어 보세요


부끄럽지만 솔직히 말하자면 이번 페어 기간 동안에는 오로지 속도에만 초점을 맞춰서 미션을 진행했다. 코드를 작성할 때 깊은 고민을 하지 않았고, 심도 있는 학습 또한 없었다.

 

"이 옵션은 왜 사용하셨나요?"
"이 부분 코드는 왜 이렇게 작성하셨죠?"
"이 기술을 사용하신 이유가 있나요? 사용했을 때 어떤 장단점이 있을까요?"

 

저는 그냥 말하는 감자인데요, 학습 테스트 코드 따라 친 건데요, 한번만살려주세요.

 

 

나는 코드를 특정 방향으로 수정해 달라고 제안하는 내용의 리뷰보다, 코드가 이런 방식으로 작성된 이유를 묻는 코멘트에 몇십 배 더 어려움을 느낀다. 질문이 들어왔을 때 명확한 이유를 설명하기 힘든 경우가 많고, 해당 방식이 별로여서 수정을 원하는 건지, 아니면 정말 나의 깊이 있는 학습을 돕기 위한 의도인지 파악하는 것에도 고민을 많이 쏟게 되는 것이 원인이다.

 

이번 리뷰어는 후자에 해당되는 코멘트를 많이 달아 주시는 분이었다. 리뷰를 전체적으로 읽었을 때는 어떻게 답을 드려야 할지 너무 막막했는데, 각 질문에 담긴 키워드들에 대해 공부하고, 답변을 해나가는 과정들이 나의 부족한 지식들을 메꾸는 것에 큰 도움이었다.

 

❓GenerationType에는 어떤 종류가 있고 왜 IDENTITY를 선택했나요

우선 GenerationType은 기본키 생성 전략을 정의한 enum으로 다음과 같은 다섯 가지 옵션을 가지고 있다.

 

1. TABLE

member에 TABLE 옵션 적용 시 테이블 생성 쿼리

 

TABLE 옵션을 적용하면 애플리케이션 실행 시 위와 같은 시퀀스 테이블을 생성한다.

 

그리고 새로운 엔티티가 영속화될 때면 위와 같이 시퀀스 테이블의 next_val 데이터를 조회 및 업데이트 한 후, Member 테이블에 대해 삽입 쿼리를 수행한다. 이 방식은 모든 데이터베이스에서 사용할 수 있지만, 성능 측면에서 다른 전략에 비해 느릴 수 있다.

2. SEQUENCE

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="my_seq")
    @SequenceGenerator(name="my_seq",sequenceName="MY_SEQ", allocationSize=1)
    private Long id;
 }

 

SEQUENCE 옵션을 적용하면 위와 같이 데이터베이스에 sequence를 생성하고

 

Member에 데이터가 추가되면 해당 시퀀스의 다음 값을 조회한 후 ID에 적용한다. 해당 전략은 시퀀스를 지원하는 데이터베이스에서 사용할 수 있다.

3. IDENTITY 

학습 테스트에 있어서 적용한 옵션으로, 테이블 생성 쿼리 작성 시 익숙하게 적용하는 auto-increament 컬럼 기능을 이용한다.

 

별도의 시퀀스나 테이블 생성 없이 기본키 자체에 옵션을 지정해 주어서 위 두 방식과 달리 추가적인 조회 쿼리가 발생하지 않는다. 가장 직관적이고 간단한 방식으로 ID 값을 자동으로 관리할 수 있다는 장점이 있어 이 옵션을 적용했다.

 

❓@OneToMany를 쓸 때 주의해야 할 점은 무엇일까요

1. 연관관계 주인 설정

데이터베이스 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리하는 반면, 엔티티를 양방향 관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나를 사용한다.

 

이렇게 엔티티와 테이블 간 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하고, 연관관계의 주인인 외래 키 관리자를 설정해 주는 mappedBy 옵션을 @OneToMany에 적용해 주어야 한다. 

 

2. 연관관계 주인에 값을 입력하자

양방향 연관관계에서는 연관관계 주인만이 외래 키의 값을 변경할 수 있기 때문에, @OneToMany를 사요할시에 연관관계 주인이 아닌곳에만 값을 입력하면 데이터가 올바르게 저장되지 않는 문제가 발생한다.

 

실제로 연관관계 주인이 아닌 엔티티에만 값을 추가했을 때 데이터베이스에 반영이 되지 않는지 확인하기 위해 테스트를 작성했다.

 

그리고 실행 결과를 확인하니 값 조회를 위한 select 쿼리들 이전에 마지막으로 수행된 삽입 쿼리는 m1 member에 대한 것이고, reservation은 추가로 저장되지 않았다.

 

 

반대로 위와 같이 연관관계 주인인 Reservation에 m1을 필드로 넣어서 EntityManager에게 주었을 때는 m1에 두 개의 예약이 모두 저장되어 있는 것을 확인할 수 있다.

 

❓ManyToOne의 기본 fetchType에 대해 아시나요

아니요.

 

OX로 대답하고 싶은 마음을 누르고 fetchType에 대해 공부해 봤다. 

 

@ManyToOne 어노테이션 파일에서 볼 수 있듯 기본 FetchType은 EAGER를 적용하고 있다. FetchType enum은 LAZY와 EAGER 두 개의 옵션을 가지고 있다.

 

LAZY Fecthed

LAZY 전략은 해당 엔티티의 연관된 엔티티를 즉시 로드하지 않는다. 대신, 해당 연관 엔티티에 접근이 시도될 때 (ex. getter 메서드를 통해 접근) 해당 연관 엔티티를 로드한다. 연관된 데이터를 항상 필요로 하는 것이 아니기 때문에 성능 최적화에 유리할 수 있다.

EAGER Fetched

EAGER 전략은 엔티티를 조회할 때 그와 연관된 다른 모든 엔티티들도 즉시 로드한다. 이는 편리할 수 있지만, 필요하지 않은 데이터까지 로드하기 때문에 성능 저하를 일으킬 수 있다. 특히, 연관된 엔티티가 많거나, 연관된 엔티티들 사이에 또 다른 많은 연관관계가 있을 경우, 이러한 성능 저하는 더욱 심각해 질 수 있다.

 

따라서 성능 최적화를 위해 @ManyToOne에 지연 로딩을 적용하도록 코드를 수정했다.