Spring Data

[JPA] JPA를 사용해서 API 개발 시 주의 사항 - 1편(Entity반환)

coding-knowjam(코딩노잼) 2021. 10. 20.
728x90

안녕하세요 Coding-Knowjam입니다.

오늘은 JPA를 사용해서 API를 개발할 때 주의해야 할 점에 대해서 알아보겠습니다.

 

1. JPA를 사용 한 API 개발 시 주의사항

여러 가지 주의해야 할 점이 있지만 가장 중요한 건 엔티티를 그대로 반환하면 안 된다는 점입니다.

결론을 먼저 말씀드리자면 엔티티를 그대로 반환하면 추후에 엔티티에 변경사항이 발생할 때, API의 스펙이 변하기 때문에 해당 API를 사용하는 여러 곳에서 문제가 발생하게 됩니다.

한번 코드로 살펴보겠습니다.

 

1.1 클래스 생성

1.1.1 Developer Entity

package org.codingnojam.springbootjpastudy.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Developer {

	@Id
	@GeneratedValue
	@Column(name = "developer_id")
	private Long id;

	private String name;

	private int age;

	@ManyToOne(fetch = FetchType.EAGER)
	private Company company;
}

 

1.1.2 Company Entity

package org.codingnojam.springbootjpastudy.domain;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Company {

	@Id
	@GeneratedValue
	@Column(name = "company_id")
	private Long id;

	private String name;
}

간단하게 Delveloper, Company 2개의 엔티티를 생성해주었습니다.

둘이 관계는 N:1 관계로 하나의 회사는 여러 명의 개발자를 가질 수 있습니다.

이제 엔티티를 DB에 저장할 수 있도록 간단하게 repository를 만들어주겠습니다.

 

1.1.3 DeveloperRepository

package org.codingnojam.springbootjpastudy.repository;

import javax.persistence.EntityManager;

import org.codingnojam.springbootjpastudy.domain.Developer;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Repository
@Transactional
@RequiredArgsConstructor
public class DeveloperRepository {

	private final EntityManager	em;

	public Developer findOneById(Long id) {
		return em.find(Developer.class, id);
	}

	public Long save(Developer developer) {
		em.persist(developer);
		return developer.getId();
	}
}

 

1.1.4 CompanyRepository

package org.codingnojam.springbootjpastudy.repository;

import javax.persistence.EntityManager;

import org.codingnojam.springbootjpastudy.domain.Company;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Transactional
@Repository
@RequiredArgsConstructor
public class CompanyRepository {

	private final EntityManager em;

	public Long save(Company company) {
		em.persist(company);
		return company.getId();
	}
}

CompanyRepository와 DeveloperRepository에 저장을 위한 save() 메서드와 Developer 객체 단건 조회를 위한 메서드를 간단하게 작성해주었습니다.

이제 컨트롤러에서 한번 객체를 저장하고 조회를 해보겠습니다.

 

1.1.5 DeveloperController

package org.codingnojam.springbootjpastudy.api;

import org.codingnojam.springbootjpastudy.domain.Company;
import org.codingnojam.springbootjpastudy.domain.Developer;
import org.codingnojam.springbootjpastudy.repository.CompanyRepository;
import org.codingnojam.springbootjpastudy.repository.DeveloperRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequiredArgsConstructor
public class DeveloperController {

	private final DeveloperRepository developerRepository;
	private final CompanyRepository companyRepository;

	@GetMapping("/developer-init")
	public Developer initDeveloper() {
		// 개발자 객체 생성
		Developer developer = new Developer();
		developer.setAge(30);
		developer.setName("coding-nojam");

		// 회사 객체 생성
		Company company = new Company();
		company.setName("woowahan");
		companyRepository.save(company);
		developer.setCompany(company);
		Long id = developerRepository.save(developer);
		return developerRepository.findOneById(id);
	}
}

컨트롤러에서는 개발자와 회사 객체를 생성하고 둘 사이의 연관관계를 매핑한 후 DB에 등록하고, id값을 통해 다시 조회하여 조회된 엔티티를 바로 반환하는 단순한 로직입니다.

포스트맨을 통해서 실행해서 결과를 보겠습니다.

 

1.2 API 실행 결과

API 실행 결과

해당 결과 화면을 보면 엔티티를 바로 반환하였기 때문에 Developer와 Comapny 엔티티의 정보들이 그대로 노출되는 것을 볼 수 있습니다. 여기서 만약에 Developer 엔티티에 job이라는 칼럼이 추가되면 어떻게 될까요?

 

API 실행 결과 2

단순히 job이라는 칼럼만 추가가 되었으니 당연히 값은 null로 들어옵니다. 만약에 해당 API를 다른 시스템에서 사용하고 있다면 당황스러울 겁니다. 갑자기 API의 스펙에 job이 추가돼버렸으니 그로 인한 영향들이 발생할 수도 있습니다.

또한 사용자는 엔티티들의 id값은 필요 없고 다른 칼럼의 값만 필요할 수도 있습니다.

이러한 이유들로 인해서 API를 개발할 때 엔티티를 그대로 반환하면 안 됩니다.

그러면 어떻게 데이터를 전달해줘야 할까요??

바로 DTO(Data Transefer Object)를 생성해서 엔티의 값을 DTO로 변환해서 전달해주면 됩니다.

코드로 살펴보겠습니다.

 

2. Entity를 DTO로 변환

2.1 DTO 클래스 생성

2.1.1 DeveloperController

package org.codingnojam.springbootjpastudy.api;

import org.codingnojam.springbootjpastudy.domain.Company;
import org.codingnojam.springbootjpastudy.domain.Developer;
import org.codingnojam.springbootjpastudy.repository.CompanyRepository;
import org.codingnojam.springbootjpastudy.repository.DeveloperRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequiredArgsConstructor
public class DeveloperController {

	private final DeveloperRepository developerRepository;
	private final CompanyRepository companyRepository;

	@GetMapping("/developer-init")
	public DeveloperDTO initDeveloper() {
		// 개발자 객체 생성
		Developer developer = new Developer();
		developer.setAge(30);
		developer.setName("coding-nojam");
		developer.setJob("Back-end");


		// 회사 객체 생성
		Company company = new Company();
		company.setName("woowahan");
		companyRepository.save(company);
		developer.setCompany(company);
		Long id = developerRepository.save(developer);
		Developer findDeveloper = developerRepository.findOneById(id);
		DeveloperDTO developerDTO = new DeveloperDTO(findDeveloper);
		return developerDTO;

	}

	@Getter
	@Setter
	static class DeveloperDTO{
		Long id;
		String name;
		int age;
		String job;
		String companyName;

		public DeveloperDTO(Developer developer) {
			this.id = developer.getId();
			this.name = developer.getName();
			this.age = developer.getAge();
			this.job = developer.getJob();
			this.companyName = developer.getCompany().getName();
		}
	}
}

DeveloperDTO를 테스트를 위해 간단하게 내부 클래스로 생성하고, 생성시점에 Developer를 받아서 필드들을 초기화하고, DTO클래스를 반환해주었습니다.

 

2.2 DTO로 변환 후 API 실행 결과

API 실행결과 DTO

앞서 엔티티를 반환했던 것과 달리 DTO로 반환하면 원하는 형태로 반환할 수 있고, 엔티티에 필드가 추가돼도 API스펙은 변하지 않습니다. (물론 컬럼의 변경과 삭제 시에는 영향을 받습니다.)

또한 컬럼명 대신 원하는 이름으로 키값을 전달할 수도 있습니다. 기존에는 company.name와 같이 접근했던 것을 DTO를 사용하니 companyName으로 키값을 전달해 줄 수 있습니다.

이러한 이유로 인해 엔티티를 반환하지 말고 꼭 DTO를 통해서 반환하도록 해야 합니다.

 

3. 정리

  • 엔티티에 변경사항 발생 시 API 스펙이 바뀌므로 DTO클래스를 반환할 것
  • DTO로 반환하면 원하는 정보들만 반환할 수 있다는 장점이 있음
  • 그러므로 JPA를 사용해서 API를 개발할 때 절대 엔티티를 반환하지 말 것

728x90

댓글