[JPA] SQL중심 개발의 문제점

Swim Lee·2021년 1월 25일
8

JPA

목록 보기
1/10
post-thumbnail

배경

현재 대부분의 현대적인 애플리케이션 개발시 객체 지향 언어를 사용함 (Java, Scala...)

데이터를 저장하기 위해서 관계형 DB를 사용 (Oracle, MySQL,...)
아직 데이터 베이스 세계의 헤게모니는 관계형 DB가 쥐고있음
필요에 의해 NoSQL같은 것을 사용하긴 해도, 중요한 데이터를 저장한다고 했을 때 99%는 관계형 DB를 선택할 것임

결국 지금 시대는 객체관계형 DB에 관리한다.

문제는 뭐냐!

분명 애플리케이션은 객체지향적으로 설계하고 개발하는데, 막상 코드를 까보면 전부 SQL!!
왜냐하면, DB는 SQL만 알아들을 수 있기 때문...
관계형 DB가 알아들을 수 있는 SQL을 계속 작성해서 날려야함
➡ 결국 SQL 중심적인 개발을 하게됨

SQL 중심적인 개발의 문제점

무한 반복, 지루한 코드

  • 기능 하나 추가해서 테이블을 하나 만들더라고, CRUD 쿼리를 다 짜야한다.
  • 또 자바 객체를 SQL로 바꾸고, SQL을 자바 객체로 바꾸는 등의 반복적인 작업을 해야함!
    (물론 요즘은 MyBatis나 스프링이 제공하는 JDBC Template 등으로 Mapping 작업이 줄었다고 해도, 결국 개발자가 쿼리를 다 작성해야함)

객체 CRUD

예를 하나 들어보자,

기획자가 회원을 설계해달라고해서 기획서를 주었다.

우리가 할일, 회원테이블 만들고, 회원 객체 만들고, DB에 넣고 빼려면 쿼리까지 작성해야함(등록 조회 수정 ...)

public class Member {
    private String memberId;
    private String name;
    ...
}
INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES ...
SELECT MEMBER_ID, NAME FROM MEMBER M
UPDATE MEMBER SET ...

위와 같이 테이블 다 만들어 놓고, 쿼리 다짜놨는데, 갑자기 연락처 컬럼을 추가해달라고 요청이옴

public class Member {
    private String memberId;
    private String name;
    private String tel;
    ...
}
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES ...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ... TEL = ?

그러면 객체에 연락처 컬럼 넣고,
테이블에도 ALTER로 연락처 컬럼 넣고,
쿼리들도 그에 맞춰서 한땀 한땀 수정해야함 ➡ 실수 발생 확률 높음

결국, 관계형 DB를 사용하는 이 상황에서는 SQL에 의존적인 개발을 피하기 어렵다

패러다임의 불일치

객체 vs 관계형 데이터베이스

단순한 SQL 작성문제를 넘어서서 패러다임의 불일치라는 문제가 있음

애초에 관계형 DB가 나온 사상이랑, 객체지향이 나온 사상이 완전 다름

  • 관계형 DB는 데이터를 잘 정규화해서 보관하는 것이 목표
  • 객체는 속성과 메서드로 잘 캡슐화해서 사용하는 것이 목표

패러다임이 안맞는 객체를 관계형 DB에 넣으려하다보니 여러가지 문제가 생김

객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공

객체를 영구 보관하는 다양한 저장소

생각을 바꿔서 객체가 먼저있고, 다음으로 객체를 저장할 저장소를 고른다고 생각해보자

RDB, NoSQL, File....

하지만 현실적으로 관계형 DB를 대신할만한 대안이 없다.

파일에 저장한다치면, 데이터 수십 수백만건을 다 Object로 바꾼후 검색해야함. NoSQL도 대안이 될 수 있기는 하지만, 아직까지는 메인으로 사용하기 힘듬

객체를 관계형 데이터베이스에 저장

객체를 RDB에 저장하기 위해서, SQL로 바꿔야하기 때문에 결국 SQL을 짜야함.

그걸 누가하냐?!
바로 우리! ㅋㅋㅋㅋ 개발자들이 해야한다 😂

개발자 = SQL 매퍼

객체와 관계형 데이터베이스의 차이

  1. 상속 (객체에만 존재)
  2. 연관관계 (객체는 참조를 가지고 연관된 객체를 가져올 수 있음, DB는 PK와 FK로 조인해서 필요한 데이터 가져옴)
  3. 데이터 타입
  4. 데이터 식별 방법

위와 같이 객체와 관계형 DB는 많은 차이가 있음

상속

그렇다면 객체의 상속관계를 객체와 차이나는 관계형 DB에 어떻게 밀어넣을 것이냐!
▶ 어려우니까 대부분 포기함...

RDB에 그나마 비슷한 것이 슈퍼타입 서브타입
어찌어찌해서 DB에 매핑해서 객체 밀어넣었다해도, 조회할 때 굉장히 번잡함

💥 Album 조회

  1. 각각의 테이블에 따른 조인 SQL 작성...
  2. 각각의 객체 생성...
  3. 상상만해도 복잡
  4. 더 이상의 설명은 생략...
  5. 그래서 DB에 저장할 객체에는 상속관계 안쓴다

객체를 RDB가 아닌 자바 컬렉션에 저장한다면?

list.add(album); 으로 끝남

자바 컬렉션에서 조회한다면?

Album album = list.get(albumId); 그냥 get으로 앨범객체 꺼내면 됨

심지어 객체를 바로 꺼내는 것이기 때문에
필요하다면 부모타입으로 조회 후 다형성 활용도 가능

Item item = list.get(albumId);

그냥 자바 컬렉션에 넣을 때는 문제가 정말 단순해지는데, 패러다임이 다른 RDB에 넣고 빼려니 복잡함 (SQL 매핑해주고, 객체로 변환해주고...)

연관관계

  • 객체는 참조를 사용 : member.getTeam()
  • 테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID

객체는 참조로 연관된 객체를 찾아갈 수 있다.
그런데 테이블은 외래키 사용함. MEMBER에서 TEAM 가고싶다면, MEMBER에 있는 TEAM_ID(FK)와 TEAM의 PK를 JOIN하면 됨.

그런데 재밌는 것은, 객체는 Member에서 Team으로는 갈 수 있지만, 역으로 Team에서 Member로는 가지 못함 (반대방향으로 참조가 없기 때문)

하지만 테이블의 경우 PK와 FK로 JOIN하는 것이기 때문에 양방향으로 가능

두 개 서로 차이 있다는 것 알아두기

💥 객체를 테이블에 맞추어 모델링

▶ 보통 객체를 테이블에 맞추어서 모델링을 함

class Member {
    String id; //MEMBER_ID 컬럼 사용
    Long teamId; //TEAM_ID FK 컬럼 사용
    String username; //USERNAME 컬럼 사용
}

class Team {
    Long id; //TEAM_ID PK 사용
    String name;  //Name 컬럼 사용
}

💥 테이블에 맞춘 객체 저장

class Member {
    String id; //MEMBER_ID 컬럼 사용
    Long teamId; //TEAM_ID FK 컬럼 사용
    String username; //USERNAME 컬럼 사용
}

INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...

▶ 인서트 쿼리에 컬럼과 필드 매핑해서 쭉쭉 넣어주면 됨 (보통 MyBatis 같은 것들이 이러한 작업 편리하게 해줌)

하지만!!

뭔가 객체지향스럽지 않음 😒
Member가 외래키 값을 갖는게 아니라 Team객체에 대한 참조를 가지는게 맞지 않나?

아래와 같이 객체 설계를 바꿔봄

객체다운 모델링

class Member {
    String id; //MEMBER_ID 컬럼 사용
    Team team; // "참조로 연관관계를 맺는다!!!!"
    String username;//USERNAME 컬럼 사용
    
    Team getTeam() {
    	return team;
    }
}

class Team {
    Long id; //TEAM_ID PK 사용
    String name; //NAME 컬럼 사용
}

객체 모델링 저장

class Member {
    String id; //MEMBER_ID 컬럼 사용
    Team team; // "참조로 연관관계를 맺는다!!!!"
    String username;//USERNAME 컬럼 사용
    
    Team getTeam() {
    	return team;
    }
}
                   **  member.getTeam().getId(); **

INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES...

테이블에 필드 매핑해서 넣어야하는데, 객체 모델링에는 외래키 값을 나타내는 필드가 없음. Team 객체에대한 참조만 존재함.
▶ 조금 번거롭지만, Team에 대한 참조를 타고가서 Id를 가져와서 FK로 넣어줌. 저장 성공적으로 잘함 😁

하지만 객체지향 모델링의 문제점은 조회에서 발생

객체 모델링 조회

SELECT M.*, T.*
   FROM MEMBER M
   JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
public Member find(String memberId) {
    //SQL 실행
    Member member = new Member();
    //DB에서 조회한 회원 관련 정보를 모두 입력
    Team team = new Team();
    //DB에서 조회한 팀 관련 정보를 모두 입력
    
    //회원과 팀 관계 설정
    member.setTeam(team);
    return member;
}

조인해서 가져온 회원과 팀 각각의 정보를 각가의 객체에 넣어주고, 마지막으로 setTeam(team)으로 연관관계 설정까지 개발자가 해줘야함
▶ 굉장히 번거로움, SQL중심으로 설계하면 그냥 바로 맞는 필드에 때려넣으면 되는데, 구분해서 가져온 후 연관관계까지 설정해줘야함
▶ 따라서 보통 member랑 team 함께 조회할 경우 MemberTeam이라는 큰 클래스(DTO) 만들어서 응답값 일자로 때려박음... 그리고 객체하나로 반환하고 이런식으로 많이 했음

객체 모델링, 자바 컬렉션에 관리

▶ 자바 컬렉션으로 관리한다하면, 객체지향적으로 설계하는 것이 참 괜찮음

list.add(member);

Member member = list.get(memberId);
Team team = member.getTeam();

컬렉션에 member 넣고, 필요하면 member 꺼낼때!
컬렉션에 member넣을 때, 연관된 team도 자동으로 들어감 (RDB에 넣듯이 FK에 member.getTeam().getId() 이런식으로 매핑 안해줘도됨)
컬렉션에서 team 꺼낼 때, 그냥 member에서 연관 필드 가져오면 됨 (RDB에서 join으로 가져온 team정보를 일일이 필드에 입력안해줘도됨)

코드 한줄로 끝남! 이거를 RDB에 넣는 순간 이 모든게 헝클어짐 😥
중간에 엄청난 번잡한 일들을 개발자가 해야함, 생산성이 안나옴

객체 그래프 탐색

객체는 자유롭게 객체 그래프를 탐색할 수 있어야함

서로 참조가 있는 객체들은 자유롭게 해당 참조를 타고 조회 가능해야함
member.getTeam()
member.getOrder().getDelievery() 이런식으로

그러나 문제가 있다!!!
정말 저 객체그래프들을 위에 나온 대로 마음 껏 호출할 수 있는가?
▶ NOPE

WHY??

처음 실행하는 SQL에 따라 탐색범위 결정되버림

SELECT M.*, T.*
  FROM MEMBER M
  JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getTeam(); //OK
member.getOrder(); //null

처음 SQL 날릴 때 member랑 team만 가져옴, 그리고 member랑 team만 값을 채워넣어서 반환
▶ member.getTeam()은 가져온 데이터라 Ok, 하지만 order은 SQL로 조회하지 않음 null값임
▶ member에 order 필드가 있다고해서 마음껏 값을 꺼낼 수 없음. 결국 코드 다 까봐야함 (order조회 해왔나 안해왔나 확인하고 써야함...)

엔티티 신뢰문제로 이어짐

class MemberService {
    public void process() {
        Member member = memberDAO.find(memberId);
        member.getTeam(); //???
        member.getOrder().getDelievery(); //???
    }
}

memberDAO를 통해 조회해온 member 엔티티에 Team과 Order, 그리고 Delievery라는 연관관계들 있지만, memberDAO에서 해당 데이터를 조회해왔는지 확실히 알지못하기 때문에 자유롭게 호출할 수 없음

해당 memberDAO코드를 눈으로 확인하지 않는 이상, 반환된 엔티티 객체를 신뢰하고 쓸 수 없음

레이어드 아키텍쳐에서는 그 다음 계층에 대해서 신뢰를 하고 있어야함
물리적으로는 서비스 DAO로 나누어져있지만, 논리적으로는 엮여있음. 내가 직접 DAO계층의 코드를 까보지 않는이상 해당 계층이 반환한 엔티티에 대해 신뢰할 수 없음. 의존성 좋지않음

그렇다고해서 모든 객체를 미리 로딩할 수는 없다

member 조회할 때마다 모든 연관된 객체 다 끌고올 수도 없음

따라서 대안으로 상황에 따라 동일한 회원 조회 메서드를 여러벌 생성

memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam(); //Member와 Team 조회
memberDAO.getMemberWithOrderWithDelievery(); //Member, Order, Delievery 조회

SQL을 직접 다루게 되면, 계층형 아키텍쳐에서 진정한 의미의 계층 분할이 어렵다.

비교하기

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2; //다르다

class MemberDAO {
    
    public Member getMember(String memberId) {
    	String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
        ...
        //JDBC API, SQL 실행
        return new Member(...); //새로운 객체 생성해서 반환하니까 == 비교 당연히 다름
    }
}

일반적인 SQL을 사용해서 조회시, 반환되는 객체는 ==비교했을 때 다르다

비교하기 - 자바 컬렉션에서 조회

String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; //같다

자바 컬렉션에서 같은 key값으로 조회한 객체는 동일하다. (항상 같은 참조 반환)
따라서 == 비교하면 true 나옴

결론

객체답게 모델링 할수록 매핑작업만 늘어난다

내가 객체지향적인 것을 배우고, 객체지향적으로 설계를 하면 할수록 오히려 더 번잡한 작업만 늘어남

그래서 SQL에 맞춰서 객체를 설계하고 데이터 전송하는 역할(DTO)만 하도록 할 수 밖에 없음. (그렇게 안할 수 있지만, 엄청나게 많은 코드를 작성해야함... 가성비 안나옴)

이미 1980년대부터 아래와 같은 고민함

객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수는 없을까?

자바 진영에서는 이러한 고민의 결과로 JPA(Java Persistence API)가 나옴


해당 게시글은 인프런 김영한님의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>을 듣고 정리한 내용입니다.

profile
백엔드 꿈나무 🐥

2개의 댓글

comment-user-thumbnail
2021년 1월 26일

잘보고 갑니다 ㅎㅎ

답글 달기
comment-user-thumbnail
2021년 7월 26일

좋은 글 감사합니다, 많은 도움 됐습니다!

답글 달기