프로젝트 JPA 도입기 1 - 객체를 객체답게 사용하기

송은석·2023년 4월 27일
0

Directors

목록 보기
4/6

도메인 객체를 인메모리로 자바 컬렉션을 통해 관리하다가, JPA와 MySQL DB를 도입하면서 달라지게 된 점을 공유합니다. JPA와 MySQL의 세부적인 설정에 대해서는 다루지 않습니다.

🖥️ 이 포스트는 Directors 프로젝트 진행 중 작성되었습니다. 진행 중인 프로젝트를 보고 싶으시다면 => Directors


프로젝트 초반: Java Collection으로 도메인 관리

프로젝트 초반에는 DB를 붙이지 않고, 컬렉션을 통해 데이터를 관리하면서 로직을 작성했습니다.

예를 들어 아래의 클래스처럼 도메인 객체를 정의하고,

// ... (생략)

@AllArgsConstructor
@Builder
@Getter
public class User {
    private final String id;

    private String password;

    private String name;

    private String nickname;

    private String email;

    // ... (생략)
}

이를 아래와 같은 Inmemory Collection을 사용한 Repository를 통해 관리한 것입니다.
// ... (생략)

@Repository
public class InmemoryUserRepository implements UserRepository {
    private final Map<String, User> userMap = new HashMap<>();

    @Override
    public Optional<User> findUserByIdAndUserStatus(String id, UserStatus userStatus) {
        User user = userMap.get(id);
        return Optional.ofNullable(user).filter(u -> u.getStatus().equals(userStatus));
    }

    @Override
    public void saveUser(User user) {
        userMap.put(user.getUserId(), user);
    }
    
    // ... (생략)
}



JPA가 뭐죠?

기능이 어느 정도 구현되고 나서, 애플리케이션에 DB를 연동하게 되었습니다.

DB를 어떤 방식으로 붙여볼까 고민하다가 JPA라는 기술을 알게 되었고, 이를 이해해보기 위해 김영한님이 쓰신 책 "자바 ORM 표준 JPA 프로그래밍"을 읽어 보았습니다.

책에서 흥미로웠던 내용 중 하나는, 자바의 객체 지향 프로그래밍과 관계형 데이터 베이스 간의 패러다임 불일치에 관한 것이었습니다.

객체 참조를 통해 연관 관계를 맺는 객체 지향 프로그래밍의 패러다임과 외래 키(PK)를 통해 연관 관계를 맺는 관계형 데이터 베이스의 패러다임 간의 차이로 인해, 두 패러다임을 하나의 애플리케이션에서 사용해야 하는 개발자가 중간에서의 패러다임 변환을 위해 너무 많은 시간과 코드를 소비하게 된다는 것이었습니다.

이에 저의 코드도 객체 참조를 잘 활용하면서 작성되고 있었는지를 돌아보게 되었습니다.

객체 참조를 활용하지 못했던 코드들

작성했던 코드들을 다시 확인하면서 느낀 점은, 객체를 사용하고 있었음에도 객체 참조를 잘 활용하지 못하고 있었다는 것이었습니다.

다음 getDirector 메소드는, 매개 변수로 주어지는 directorId를 통해 해당 값을 식별자로 가지고 있는 User 도메인 객체와 연관된 다른 도메인 객체들을 모아서 반환해주는 기능을 수행합니다.

// ... (생략)

    public GetDirectorResponse getDirector(String directorId) {
        var director = getDirectByDirectorId(directorId);
        
        var address = getAddressByDirectorId(director.getUserId());
        var specialtyInfoList = getSpecialtyInfoListByDirectorId(director.getUserId());
        
        return new GetDirectorResponse(
        	director.getName(), director.getNickname(), address, specialtyInfoList
        );
    }

// ... (생략)

메서드에서 내부적으로 여러 private 메소드들을 사용하고 있는 것을 볼 수 있는데요, 모든 메소드들이 처음 getDirectByDirectorId 메소드를 통해 조회한 director의 userId 값을 동일하게 매개 변수로 사용하고 있는 것을 볼 수가 있습니다.
// directorId를 통해 연관된 다른 도메인 Repository들에 접근해 데이터를 가져오고 있습니다.
    
    private User getDirectByDirectorId(String directorId) {
        return userRepository
                .findByIdAndUserStatus(directorId, UserStatus.JOINED)
                .orElseThrow(NotFoundException::new);
    }

    private Address getAddressByDirectorId(String directorId) {
        return userRegionRepository
                .findByUserId(directorId)
                .orElseThrow(NotFoundException::new)
                .getAddress();
    }

    private List<SpecialtyInfo> getSpecialtyInfoListByDirectorId(String directorId) {
        return specialtyRepository
                .findByUserId(directorId)
                .stream()
                .map(Specialty::getSpecialtyInfo)
                .collect(Collectors.toList());
    }

그러나 위에서 살펴보았듯 이러한 방식은 객체 지향 프로그래밍의 객체 참조 방식이라기 보다는, 마치 관계형 데이터 베이스가 PK를 사용하여 저장된 데이터를 조회하는 방식과 유사합니다.

결국 하나의 User 객체에 연관된 다른 도메인들을 조회하기 위해, 연관된 모든 도메인들의 Repository에 접근하는 코드들을 작성하고 있었던 것입니다.

코드에서 이러한 문제를 발견하고 나서, JPA를 통해 이 문제들을 해결해보고 싶다는 생각을 하게 되었습니다. JPA는 연관되는 객체들 간에 연관 매핑을 맺어줄 수 있고, 영속성 컨텍스트를 통해 조회한 엔티티에 연관 매핑된 객체들에 대해 내부적인 Fetch Join을 통해 객체 참조를 사용할 수 있기 때문입니다.

그래서, JPA를 사용해보았습니다.



JPA의 연관 매핑 사용해보기

JPA 어노테이션을 통해 User 엔티티가 Specialty, UserRegion 엔티티와 연관 관계를 맺을 수 있도록 코드를 변경해주었습니다.

// ... (생략)
  
@Entity
@Table(name = "users")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
public class User extends BaseEntity {
    
    @Id
    private String id;

    private String password;

    private String name;

    private String nickname;

    private String email;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Specialty> specialtyList = new ArrayList<>();

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private UserRegion userRegion;

    public Address getUserAddress() {
        return userRegion.getAddress();
    }

    public List<SpecialtyInfo> getSpecialtyInfoList() {
        return specialtyList
                .stream()
                .map(Specialty::getSpecialtyInfo)
                .collect(Collectors.toUnmodifiableList());
    }
  
    // ... (생략)
}

그리고 이를 통해서 위에서 작성했던 getDirector의 메소드 로직 또한 변경해주었습니다.
    @Transactional
    public GetDirectorResponse getDirector(String directorId) {
        var director = getJoinedUserByUserId(directorId);

        return new GetDirectorResponse(
           director.getName(), 
           director.getNickname(), 
           director.getUserAddress(), 
           director.getSpecialtyInfoList()
        );
    }

연관 매핑을 사용하게 되면서 코드가 한층 간결해진 것을 볼 수 있습니다.

User 도메인 내부에서 연관된 도메인과 연관 매핑을 해주고, 이를 Fetch Join을 통해 받아오면서 객체 참조를 편리하게 이용할 수 있게 되었습니다.


그냥 SQL로 Join 쓰면 되지 않나요?💁‍♂️

한편으로는 SQL로 Join 쿼리를 통해 조회한 것과 무엇이 다르냐! 라고 생각해볼 수도 있을 것 같습니다:)

그러나 JPA의 연관 매핑은 직접 SQL을 사용하는 것에 비해 다음 부분에서 분명한 강점을 가집니다.

  1. 객체 참조를 통해 조회되는 엔티티를 신뢰할 수 있습니다.
    객체 내부에 연관 관계를 매핑해주었다면, 내부적으로 JPA에 의해 Join 쿼리가 작성되어 엔티티 클래스에 맞게 DB로부터 데이터를 가져와 매핑해주게 됩니다. 따라서 개발자는 JPA를 통해 조회한 엔티티에 관하여 항상 올바른 값이 매핑되어 있을 것임을 신뢰할 수 있게 됩니다.

  2. 도메인 객체에 변경이 발생했을 때, 변경되는 조회 쿼리를 개발자는 신경쓰지 않아도 됩니다.
    객체와 DB 테이블이 매핑되어 있으므로, 변경 사항에 맞게 도메인 객체를 수정하고 어노테이션을 적용해준다면 변경되는 쿼리는 JPA에 의해 내부적으로 재구성됩니다. 이를 통해 개발자는 SQL의 변경에 신경 쓰지 않고, 오로지 객체에 대해서만 집중해서 개발할 수 있게 됩니다.
  1. 무엇보다 DB의 데이터를 객체를 통해 자연스럽게 사용할 수 있게 해줍니다.
    이를 통해 보다 객체 지향적인 프로그램을 만들 수 있게 됩니다.


😊마치며..

이번 시간에는 프로젝트에 JPA를 도입하면서 생긴 코드의 변화 및 장점에 대해 알아보았습니다.

스프링 프레임워크가 DI를 통해 객체 간의 의존성을 관리해줌으로써 보다 객체 지향적이고 Plain한 자바 코드를 작성할 수 있도록 도와주었던 것처럼,

JPA는 객체 지향 프로그래밍에서 관계형 데이터베이스를 다루는 데 있어서 객체와 테이블 간의 매핑을 자동으로 처리해줌으로써 보다 객체 지향적이고 직관적인 코드를 작성할 수 있도록 도와주는 역할을 하고 있음을 알게 되었습니다.

그리고 이를 코드로 적용해보기도 했습니다.


다음 시간에는 JPA를 사용하면서 추가적으로 변경된 부분에 대해 살펴보겠습니다.👍



참고

  • 책 | 자바 ORM 표준 JPA 프로그래밍, 김영한
profile
Done is better than perfect🔥

0개의 댓글