[JPA] 엔티티를 DTO로 변환하여 사용하기

728x90

안녕하세요🖐

 

공부를 하다가, 엔티티는 DB와 직접적으로 접근하는 영역이기 때문에

건들면 안된다는 이야기를 들었습니다

 

왜 건들면 안된다는 걸까요❓

 

그러면 엔티티에 접근을 안하고, 어떻게 DB에 접근해서 CRUD를 진행할까? 라는 생각이 먼저 들었습니다.

 

이에 대한 내용을 포스팅 해보겠습니다.

 

일단 저는 간단한 Rest API 연습을 하기 위해서 CRUD 게시판을 짜기 위해 공부중이였습니다.

 

최대한 객체지향 적으로 짜기 위해, 스프링 3Layer Architecture 를 도입해서 진행을 했습니다.

설명 : https://hyeonq.tistory.com/131

 

 

일단 Entity를 생성을 하겠습니다.

@Getter
@NoArgsConstructor
@Entity
public class Question {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment 역할
	private Long id;

	@Column(length=200, nullable=false)
	private String subject;

	@Column(columnDefinition = "TEXT", nullable=false)
	private String content;
	
	@Builder
	public Question(Long id, String subject, String content) {
		this.id = id;
		this.subject = subject;
		this.content = content;
	}
}
  • 이 클래스는 DB와 직접적으로 연동되어, 스키마를 생성하고 데이터를 관리하는 클래스 입니다.
  • @Setter 를 쓰지 않는 이유는 이 엔티티에서 어떠한 직접적인 수정도 일어나지 않게 하기 위해서 입니다.
    • 수정이 일어나게 하려면 생성자를 통해서 하거나, 메소드를 만들어서 하는 2가지 방법 밖에 없습니다
  • @NoArgsConstructor : 파라미터가 없는 디폴트 생성자를 생성
  • @Builder 롬복 어노테이션
    •  @Builder 어노테이션을 사용하여서 생성자를 하게 되면 가독성 향상 이 됩니다.
	public Question toEntity() {
		return Question.builder()
			.subject(subject)
			.content(content)
			.build();
	}

 

이런식으로 코드 작성을 할 수 있습니다.  가독성 이 좋습니다.

위 메소드는 엔티티를 -> DTO 로 변환하는 메소드 입니다. (자세한 내용은 아래서 설명하겠습니다.)

 

물론 꼭 이렇게 할 필요는 없습니다. 객체 생성 패턴은 여러가지이기 때문에 다른 방법을 사용해두 됩니다.

 

원래라면은 이제 db에접근하는 객체를 생성했으니,Repository를 생성하고 Service를 만들어서 비즈니스 로직을 만들고

컨트롤러에 코드를 짜서 View에 접근을 하든, RestAPI를 만들어 JSON 값을 넘기든 하면 끝일 것이라고 생각합니다.

 

하지만 여기서 제가 View에 접근을 한다고 말하였죠?

View에 접근하는 부분에서는 엔티티 데이터 전부가 과연 필요할까요?

저는 아니라고 생각합니다. 필요에 의해서 원하는 데이터만 뽑아서 사용할 것입니다. 

 

그렇다면 View에 표시할 때 필요한 필드들만 전송하기 위해서는 그러면 엔티티 클래스는 냅두고, 엔티티와 비슷하게 생긴

DTO를 만들어서 사용해야 합니다. 

 

 

즉 정리해서 말하면 DTO를 사용하여 엔티티와 뷰를 분리하는 주된 이유

  • 계층 간의 의존성을 최소화하고, 더 중요한 비즈니스 로직을 엔티티에 집중시키기 위함
  • 의존성 분리
    • 엔티티가 직접적으로 뷰에 의존하지 않도록 함으로써, 뷰 레이어의 변경이나 엔티티의 변경이 서로에게 영향을 미치지 않도록 합니다.
  • 보안
    • 엔티티는 데이터베이스와 관련된 많은 정보를 가지고 있습니다. 이러한 중요한 정보를 뷰에 직접 노출하는 것은 보안 상의 위험을 초래할 수 있습니다. DTO를 사용하면 엔티티의 일부 정보만 뷰에 노출시킬 수 있습니다.
  • 컨트롤
    • 엔티티는 주로 비즈니스 로직과 데이터 저장을 위한 목적으로 사용됩니다. 이에 비해 DTO는 뷰에서 필요한 데이터를 표현하는 데 중점을 둡니다. 따라서 엔티티가 뷰의 특정 요구 사항에 영향을 미치지 않도록 합니다

 

즉 엔티티의 변경이나 뷰의 변경이 서로에게 영향을 미치지 않고, 엔티티의 민감한 정보를 뷰에서 감추게 됩니다

 

다음으로 DTO를 만들어 보겠습니다. 

@Getter
@NoArgsConstructor
public class QuestionRequestDto {

	private Long id;
	private String subject;
	private String content;

	@Builder
	public QuestionRequestDto(Long id ,String subject, String content) {
		this.id = id;
		this.subject = subject;
		this.content = content;
	}

	public Question toEntity() {
		return Question.builder()
			.subject(subject)
			.content(content)
			.build();
	}
}
  • Entity와 거의 유사한 형태인 Dto를 생성했다
  • QuestionRequestdto에서도 @Builder 를 사용하여 생성자를 생성 했습니다.

 

DTO에서도 분류를 합니다.

  • 데이터를 요청하는 RequestDto
  • 데이터를 요청받는 ResponseDto

이 두가지가 있다고 생각하면 됩니다.

일단 저는 데이터를 조회하기 위해 요청하는 DTO를 만들었습니다. 

 

위 코드를 보면 아래 쪽에 아까 설명한 코드가 나와있지 않나요??

toEntity() 메소드는 Question 객체를 return 받고 Question 생성자에 @Builder 어노테이션을 활용하여

빌더패턴을 만들어 엔티티를 dto로 만드는 과정을 코드로 보여드리는 것 입니다.

 

저는 위 방법은 그냥 참고만 하라고 넣은거지, 저는 저 방법을 사용하지 않고 변환을 했습니다.

변환하는 방법은 곧 나옵니다😂

 

다시 한번 설명하지만

절대로 Entity클래스를 Request, Response 클래스로 사용해서는 안된다.

dto는 View를 위한 클래스로, 자주 변경이 되는 작업을 처리할 것이다.

Entity 클래스와 Controller에서 사용할 dto는 분리해서 사용해야한다.

 

다음으로는

JPA와 연동하는 레포지토리 입니다.

public interface QuestionRepository extends JpaRepository<Question, Long> {
}

위 인터페이스는 딱히 설명을 할게 없네요 JpaRepository를 상속받아 안에  있는 메소드를 사용합니다.

하나 주의할점은 <>안에 들어갈 내용은 무조건 <Entity, Id타입> 입니다.

 

Id타입이라고 하면은, Primary key 타입을 말하는 것 입니다. 보통 Long, Integer 둘중 하나 입니다.

절대로 Entity 자리에dto를 넣으면 안됩니다.

 

다음으로는 드디어 Service 입니다.

@RequiredArgsConstructor
@Service
public class QuestionService {
	private final QuestionRepository questionRepository;

	public List<QuestionRequestDto> getListDto() {
		List<Question> questionList = questionRepository.findAll();

		List<QuestionRequestDto> questionRequestDtos = questionList.stream()
			.map(question -> QuestionRequestDto.builder()
				.id(question.getId())
				.subject(question.getSubject())
				.content(question.getContent())
				.build())
			.collect(Collectors.toList());
		return questionRequestDtos;
	}

	//toEntity() 활용
	public List<QuestionRequestDto> getListDto2() {
		List<Question> questionList = questionRepository.findAll();

		return questionList.stream()
			.map(QuestionRequestDto::toEntity)
			.collect(Collectors.toList());
	}


}
  • 비즈니스 로직이 담긴 서비스 입니다

이 클래스에서는 이제 컨트롤러에서 사용하기 위한 로직을 작성합니다.

컨트롤러에 직접 엔티티가 접근할 수 없으므로 서비스에서 엔티티 => dto 변환 작업을 해줍니다.

	public List<QuestionRequestDto> getListDto() {
		List<Question> questionList = questionRepository.findAll();

		List<QuestionRequestDto> questionRequestDtos = questionList.stream()
			.map(question -> QuestionRequestDto.builder()
				.id(question.getId())
				.subject(question.getSubject())
				.content(question.getContent())
				.build())
			.collect(Collectors.toList());
		return questionRequestDtos;
	}
  • 일단 Question 엔티티를 담을 List에 담고, DB에 모든 Question 엔티티를 가져옵니다.
  • 그리고 dto가 담길 list를 생성하고, 스트림으로 변환합니다.
    • 🖐 스트림은 데이터를 순차적으로 처리하기 위한 일련의 요소 입니다.

  • 그리고 stream 인터페이스의 map 메서드를 사용합니다.
    • map은 각 요소에 대해 특정 작업을 수행하고 결과를 새로운 스트림으로 변환 합니다.

  • 그리고 아까 dto 클래스에 @Builder 을 선언해두었기 때문에 buillder() 메소드를 선언하고, 빌더 패턴으로 필드값을 설정합니다.
  • 그리고 마지막으로 .collect(Collectors.toList()) 는 스트림 요소들을 리스트로 변환하는 컬렉터 입니다.

그리고 변환된 객체들을 담고있는 dto를 리턴 합니다. 

 

마지막으로 컨트롤러 입니다.

@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class QuestionController {
	private final QuestionRepository questionRepository;
	private final QuestionService questionService;

	@GetMapping("/question_list")
	public List<QuestionRequestDto> list() {
		return questionService.getListDto();
	}

}

 

이제 API 테스트를 해보겠습니다.

성공적으로 DB에 있는 값들이 JSON 값으로 뜬 것을 확인 했습니다.

 

솔직히 좀 귀찮긴 하지만, 나중에 유지보수를 위해서 라면 엔티티랑 DTO를 변환하는 작업을 계속 하는 것이 좋다고  생각합니다.

 

긴 글 봐주셔서 감사합니다.

 

틀린 부분있으면 지적 해주시면 감사하겠습니다

728x90