본문 바로가기

개발

[Java/자바] JPA 에 대하여 (1) - JPA의 등장

개인적인 생각으로 개발자는 SQL 매퍼라고 생각합니다.

 

개발자의 주목적 중 하나는 데이터베이스에 정보를 저장하고 데이터베이스에서 정보를 꺼내와서 사용자에게 보여주는 역할을 위해 코드를 작성하는 것입니다.

 

사실 클린코드니, 아키텍처니, 객체지향이니, 뭐니... 등등 사실 이 모든 것은 저희가 CURD를 조금 더 잘하기 위해서 사용하는 것들이라고 생각합니다.

 

아직까지 정말 많이 사용하고 있는 데이터베이스는 관계형 데이터베이스입니다. 실제로 서비스를 개발하다 보면 객체와 데이터베이스에서 사용하는 패러다임의 차이로 인해 복잡한 상황들이 발생하곤 합니다.

 

 

 

1. 상속

 

 

자바에서는 상속관계라는 것이 있습니다.

 

다음과 같은 코드를 보겠습니다.

 

 

public abstract class Item {
    String itemId;
    String name;
    int price;
}

 

Item 객체

 

public class Album extends Item {
    String artist;

    public Album(String itemId, String name, int price) {
        super(itemId, name, price);
    }
}

 

Item을 상속받는 Album 객체

 

public class Movie extends Item {
    String director;
    String actor;

    public Movie(String itemId, String name, int price){
        super(itemId, name, price);
    }
}

 

Item을 상속받는 Movie 객체

 

 

 

위 코드를 보시면 Item 객체와 이 Item 객체의 세분류인 Album 객체와 Movie 객체가 존재합니다. 이러한 연관관계를 데이터베이스로 나타내면 다음과 같은 형태가 됩니다.

 

 

 

 

이걸 토대로 실제로  Album 데이터를 가져온다고 생각해 보겠습니다.

 

 

Item 테이블의 join SQL을 만들고 해당 객체를 따로 생성해서 값 매핑, Album 테이블의 join SQL을 만들고 해당 객체를 따로 만들어 값 매핑, 실제 서비스 로직에서 ~get(itemId) 메서드를 통해 값을 꺼내올 수 있습니다.

 

굉장히 복잡한 작업이기 때문에 실제로 DB에 저장할 객체는 상속을 사용하지 않습니다.

 

 

하지만 이러한 작업을 자바의 컬렉션으로 구현을 하면? 굉장히 단순하게 만들 수 있습니다.

 

Album album = list.get(albumId);	// album id 값을 이용해 직접 조회
Item item = list.get(albumId);		// 다형성을 활용하여 부모타입으로 조회

 

 

 

자바 컬렉션 안에 각 객체를 넣어서 관리한다면 위와 같이 데이터를 매핑하는 작업을 한 줄로 끝낼 수 있게 됩니다.

 

 

 

 

 

 

2. 참조

 

 

객체는 참조를 이용하고, 테이블은 외래키를 이용하여 join 문을 작성함으로써 연관관계를 맺을 수 있습니다.

 

 

저희의 목적은 CURD 이기 때문에 보통 테이블의 설계에 맞춰서 객체를 모델링하게 됩니다.

 

 

이번엔 외래키를 사용해서 연관관계를 맺는 테이블의 특성에 따라서 사용자 객체를 만들어 보겠습니다.

 

 

class Member{
    String id;
    Long temId;	 // team 테이블에서 사용할 외래키
    String userName;
}

class Team{
    Long id;
    String name;
}

 

위와 같이 작성하게 되면 테이블에 저장할 INSERT 문은 다음과 같이 작성할 수 있게 됩니다.

 

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

 

 

teamId라는 값을 가지고 있어야 team 테이블과의 연관관계를 찾을 수 있겠죠.

 

하지만 자바는 객체지향적 언어입니다. CURD를 위해 객체를 설계를 하게 되면 사실 위와 같은 예시는 그다지 객체지향적이지 않은 형태가 됩니다.

 

조금 더 객체지향적으로 모델링을 해봅시다.

 

 

class Member{
    String id;
    Team team;		// 참조를 통해 연관관계 매핑
    String user Name
    
    Team getTeam(){
        return team;
    }
}

class Team{
    Long id;
    String name;
}

 

 

Member 객체 안에서 바로 Team 객체를 꺼내올 수 있도록 만들었습니다. 위와 같은 방식으로 만들게 되면 실제로 getTeam()을 통해 바로 Team 객체를 가져올 수 있겠죠.

 

하지만 이렇게 작성을 한다면 INSERT문을 짤 때 문제가 발생합니다.

 

 

INSERT문에 넣은 teamId 값을 가져오기 위해서는 다음과 같이 작성할 수 있겠죠.

 

member.getTeam().getId();

 

 

뭐.. 나쁘지 않은데?

 

 

라고 생각할 수 있습니다. 하지만 데이터를 가져올 때는 더 귀찮은 작업을 해야 합니다.

 

 

연관관계가 있는 두 테이블에서 join을 통해 데이터를 한 번에 가져오는데 다음과 같은 쿼리를 날린다고 생각하겠습니다.

 

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();
    // 이후, member에 SQL을 실행하여 가져온 데이터 매핑
    // `
    // `
    // `

    Team team = new Team();
    // 이후, team에 SQL을 실행하여 가져온 데이터 매핑
    // `
    // `
    // `

    // 사용자와 팀 관계 매핑
    member.setTeam(tema);
    return member;
}

 

 

정말 딱 필요한 내용만 작성해서 그렇지 실제로 작업을 하게 되면 굉장히 귀찮은 일입니다. 

 

이러한 데이터를 관계형 데이터베이스가 아닌, 자바의 컬렉션에 저장한다면?

 

다음과 같이 저장할 수 있을 것입니다.

 

// 컬렉션에 member 객체 저장
list.add(member);


// 다른 로직에서...
Member member = list.get(memberId);
Team team = member.getTeam();

 

 

단 몇 줄 만에 데이터를 가져오는 게 가능해집니다.

 

 

 

 

3. 객체 그래프 탐색

 

객체 그래프 탐색은

 

 

 

 

Member 객체에 Order 객체에 대한 참조가 있고, Team 객체에 대한 참조도 있고, Order 객체에는 OrderItem 객체에 대한 참조가 있고, OrderItem 은 Item 객체를 참조하고 있습니다.

 

이렇게 하나의 객체로부터 자유롭게 다른 객체를 참조할 수 있어야 합니다.

 

 

하지만 관계형 데이터베이스에서 데이터를 꺼내오다 보면, 탐색할 수 있는 개체에 대한 한계가 DB에서 데이터를 가져오는 순간을 기준으로 정해집니다.

 

 

 

아까의 Member 객체에 대한 데이터를 꺼내오는 SQL을 다시 보겠습니다.

 

 

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

 

 

이 SQL을 보면 Member 그리고 Team 객체에 대한 값만 가져올 뿐 다른 객체에 대한 데이터가 존재하지 않습니다.

 

이 상태에서 다른 객체를 참조하는 코드를 짠다면 어떻게 될까요?

 

member.getTeam();	// not null
member.getOrder();	// NullPointerException

 

 

SQL문에서는 Member에 대한 객체와 Team에 대한 객체만 가져오고 있기 때문에, 달아둔 주석처럼 getOrder()를 호출하는 순간 Null이 발생하게 됩니다.

 

 

그렇다면, 여러 명이서 작업하는 프로젝트에서 상황을 생각해 봤을 때 A 개발자가 Repository로직을 작성하였고 B개발자가 작성된 Repository를 토대로 서비스 로직을 작성 중인데 무심코 getOrder()를 호출할 수 있을까요?

 

 

Null 참조를 피하려면 Repo단에 가서 날리는 SQL문을 확인한 뒤에 안심하고 호출할 수 있을 것입니다.

 

 

관계형 데이터베이스로 SQL을 날려 작업하다 보면 위 사례처럼 엔티티 신뢰 문제가 발생하게 됩니다.

 

 

그럼 데이터를 가져올 때 전체를 다 가져오면 되는 거 아니야?

 

 

라고 생각할 수 있지만 실상은 쉽지 않습니다.

 

모든 연관관계를 가지고 있는 테이블을 모두 join 한다면 정말 많은 양의 SQL문이 탄생하게 될 것이고 데이터 양이 많다면 성능 문제 또한 발생하게 됩니다.

 

따라서 전체 데이터를 한 번에 조회하는 것은 쉽지 않습니다.

 

 

4. 결론

 

위 문제들로 인해

 

객체를 자바의 컬렉션처럼 DB에 저장할 수 없을까?

 

 

라는 생각으로부터 나온 것이 JPA(Java Persistence API)입니다.

 

다음 글부터 순차적으로 JPA에 대해 알아보겠습니다.