도메인 객체를 인메모리로 자바 컬렉션을 통해 관리하다가, JPA와 MySQL DB를 도입하면서 달라지게 된 점을 공유합니다. JPA와 MySQL의 세부적인 설정에 대해서는 다루지 않습니다.
🖥️ 이 포스트는 Directors 프로젝트 진행 중 작성되었습니다. 진행 중인 프로젝트를 보고 싶으시다면 => Directors
프로젝트 초반에는 DB를 붙이지 않고, 컬렉션을 통해 데이터를 관리하면서 로직을 작성했습니다.
예를 들어 아래의 클래스처럼 도메인 객체를 정의하고,
// ... (생략)
@AllArgsConstructor
@Builder
@Getter
public class User {
private final String id;
private String password;
private String name;
private String nickname;
private String email;
// ... (생략)
}
// ... (생략)
@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);
}
// ... (생략)
}
기능이 어느 정도 구현되고 나서, 애플리케이션에 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
);
}
// ... (생략)
// 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 어노테이션을 통해 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());
}
// ... (생략)
}
@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 쿼리를 통해 조회한 것과 무엇이 다르냐! 라고 생각해볼 수도 있을 것 같습니다:)
그러나 JPA의 연관 매핑은 직접 SQL을 사용하는 것에 비해 다음 부분에서 분명한 강점을 가집니다.
이번 시간에는 프로젝트에 JPA를 도입하면서 생긴 코드의 변화 및 장점에 대해 알아보았습니다.
스프링 프레임워크가 DI를 통해 객체 간의 의존성을 관리해줌으로써 보다 객체 지향적이고 Plain한 자바 코드를 작성할 수 있도록 도와주었던 것처럼,
JPA는 객체 지향 프로그래밍에서 관계형 데이터베이스를 다루는 데 있어서 객체와 테이블 간의 매핑을 자동으로 처리해줌으로써 보다 객체 지향적이고 직관적인 코드를 작성할 수 있도록 도와주는 역할을 하고 있음을 알게 되었습니다.
그리고 이를 코드로 적용해보기도 했습니다.