[Spring boot] DTO <-> Entity 간 변환, 어느 Layer에서 하는게 좋을까?

Byuk_mm·2022년 9월 12일
19

Spring Boot Development

목록 보기
8/13
post-thumbnail

Spring Boot 개발 중 학습이 필요한 내용을 정리하고,
트러블 슈팅 과정을 기록하는 포스팅입니다.




✅ Background


📌 DTO(Data Transfer Object)에 대해

DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체를 말합니다.

Spring bootJPA를 사용하다 보면, Enitity 클래스의 중요성민감성에 대해서 잘알고 있을 것입니다. Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스이며, Entity 클래스를 기준으로 테이블이 생성되고 스키마가 변경됩니다.

그렇기 때문에 다양한 계층에서 Entity를 직접적으로 사용하게 된다면 원치 않게 Entity의 속성을 변경시킬 위험이 존재하며, Entitiy의 모든 속성이 불필요하게 외부에 노출될 가능성이 있습니다.

그렇기 때문에 우리는 DTO를 사용합니다.
Entity 클래스에서 필요한 데이터만 선택적으로 DTO에 담아서 생성해 사용함으로써, Entitiy 클래스를 감추며 보호할 수 있습니다.

때문에 DTO의 사용은 당연히 사용돼야하는 패턴입니다!


📌 DTO와 Entity의 변환

아래 코드는 Account Entity에 대한 DTO 클래스입니다.
AccountSignUpRequest DTO는 사용자의 Account Save 요청에 대한 DTO이며, AccountSignUpResponseJPA를 통해 Account save 작업이 완료된 후, 클라이언트로 리턴되는 DTO입니다.

@Getter
@NoArgsConstructor
public class AccountSignUpRequest {

    @NotBlank
    @Email(message = "이메일을 양식을 지켜주세요.")
    private String email;

    @NotBlank
    private String name;

    @NotBlank
    private String password;


    @Builder
    public AccountSignUpRequest(String email, String name, String password) {
        this.email = email;
        this.name = name;
        this.password = password;
    }

    public Account toEntity() {
        return Account.builder()
                .email(email)
                .name(name)
                .password(Password.builder().password(this.password).build())
                .build();
    }

}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AccountSignUpResponse {

    private String email;
    
    private String name;

    @Builder
    public AccountSignUpResponse(Account account){
        this.email = account.getEmail();
        this.name = account.getName();
    }
}

AccountSignUpRequest DTOtoEntity() 메소드를 통해서 DTO to Entity 작업이 가능합니다.

AccountSignUpResponse DTOBuilder 생성자를 보시면 Account를 파라미터로 받으면서 Entity to DTO 작업을 할 수 있습니다.

그렇다면 우리는 필요한 시점에 DTO to Entity 혹은 Entity to DTO 작업을 하게 됩니다. 작업을 하다가 궁금증이 생겼습니다. 이러한 DTO와 Entity 간의 변환 작업은 어느 레이어에서 하는게 좋을지에 대한 궁금증입니다.




✅ Controller Layer에서 DTO, Entity 변환 작업


@PostMapping("/test/account")
public AccountSignUpResponse signUp(@RequestBody @Valid final AccountSignUpRequest accountSignUpRequest){

	// Dto to Entity
    Account account = accountService.signUp(accountSignUpRequest.toEntity());

	// Entity to Dto
    return AccountSignUpResponse.builder().account(account).build();
}

위 코드는 Controller LayersignUp 메소드입니다.
DTO, Entity 변환 작업이 Controller Layer에서 처리되는 것을 볼 수 있습니다.

이와 같이 DTO, Entity 변환 작업이 Controller Layer에서 일어나게 되면, Controller Layer에서의 코드가 복잡해지며, DTOEntity 변환 과정에서의 비즈니스 로직이 Controller Layer에 포함되게 됩니다.

이는 즉, Controller Layer에서는 웹 계층을 처리하기 위한 코드만 존재하고 Service Layer에서 주된 비즈니스 로직이 처리돼야하는 구조를 위배하게 만들지도 모릅니다.

또한 Service Layer에서 다양한 로직을 처리한 결과를 하나의 DTO에 담아서 보내주는 것이 아니기 때문에, Controller Layer에서 여러 Service 객체를 의존하게 될 가능성이 있습니다.

하지만, Service Layer에서 Entity를 바로 받게 함으로써, Service Layer은 Entity에만 의존하기 때문에 코드 재사용성이 높아진다는 큰 장점이 있습니다.




✅ Service Layer에서 DTO, Entity 변환 작업


@PostMapping("/api/account")
public AccountSignUpResponse signUp(@RequestBody @Valid final AccountSignUpRequest accountSignUpRequest){

    AccountSignUpResponse accountSignUpResponse = accountService.signUp(accountSignUpRequest);
    
    return accountSignUpResponse;
}

위 코드는 앞선 signUp 메소드를 Service Layer에서 DTO, Entity 변환 작업이 일어나도록 바꾼 코드입니다.

Service Layer에 들어갈 때부터 DTO로 들어가며, DTO로 return 되는것을 확인 할 수 있습니다.

이와 같은 방식은 다양한 경로에서 모일 수 있는 Request DTO들이 전부다 Service Layer로 모이기 때문에 매우 heavyService Layer가 나올 수 있다는 설계적인 리스크가 있습니다.

Service Layer도 핵심 비즈니스 로직을 가지고 있는 서비스 로직과, 화면에 맞춘 읽기 전용 서비스 로직을 별도로 분리해서 설계하면 이런 문제를 어느정도 완화할 수 있습니다. 참고

또한, Service Layer가 특정 DTO에 의존하게 되기 때문에, 여러 종류의 컨트롤러에서 해당 서비스를 사용할 수 없어져서 코드 재사용성을 떨어트립니다.

하지만, 앞서 나온 Controller Layer에서의 비즈니스 로직과 같은 부분을 해결하며,
JPA에서 Lazy 조회 시 LazyInitializationException에 노출 위험을 줄여줍니다.

LazyInitializationException 예외가 발생하는 이유

  • 일반적인 백엔드 프로젝트에서 로직의 대부분은 REST API에 해당하며 실행의 흐름은 @Controller, @Service, @Repository 순서가 된다.
  • 서비스 레벨에서 @Transactional이 명시된 메써드가 종료되면 HibernateSession도 함께 종료된다.
  • FetchType.LAZY가 설정된 필드가 포함된 엔티티 오브젝트에 대해, 컨트롤러 레벨에서 해당 필드를 조회할 때 Getter 메써드를 호출하고 실제 조회 쿼리가 실행된다. 하지만 앞서 이미 Session이 종료된 상태이기 때문에 LazyInitializationException 예외가 발생하게 되는 것이다.
    참고 티스토리



✅ 결론

결론적으로 DTO, Entity 변환 작업이 Controller Layer에서 하는 방법과 Service Layer에서 하는 방법 모두 각각의 큰 장단점들이 있습니다.

그리고 분명 각각의 장단점들이 하나하나 크게 다가옵니다.
분명한 것은 프로젝트 구조와 성격이 따라 적당한 방법을 일관적으로 취하면서, 특정 상황에 따라서는 적절한 선택지를 유연하게 선택하는 것도 필요하다.

저는 현재 프로젝트에서는 LazyInitializationException의 리스크와 Controller Layer에서의 비즈니스 로직을 줄이기 위해 Service Layer에서 DTO, Entity 변환 작업을 합니다.

또한 코드 재사용성을 높이기 위해서 특정 서비스의 포맷에 맞게 변환해서 전달하는 방식을 채택해서 사용하고 있습니다. 아래 코드는 서비스에 맞게 DTO를 다시 포맷하는 예시입니다. 참고

AccountSignUpResponse accountSignUpResponse = accountService.signUp(accountSignUpRequest.toAccountServiceDto);

프로젝트 개발을 진행하면서 그 동안 일관적인 방식으로 처리하지 못했었던 DTO, Entity 변환 작업에 대해 한번더 생각해볼 수 있었고, 최선의 방식을 탐색하고 적용하는 방법을 배운 좋은 경험이었습니다.




✅ 참고

https://velog.io/@minide/Spring-boot-DTO%EC%9D%98-%EC%82%AC%EC%9A%A9-%EB%B2%94%EC%9C%84

https://jihyee.tistory.com/14

https://tecoble.techcourse.co.kr/post/2021-04-25-dto-layer-scope/

https://techblog.woowahan.com/2711/

https://studyandwrite.tistory.com/402

https://www.inflearn.com/questions/53023

https://jsonobject.tistory.com/605

profile
어디야 벽벽 / 블로그 이전 -> byuk.dev

2개의 댓글

comment-user-thumbnail
2023년 5월 17일

Account Entity 소스코드는 없나요?

답글 달기
comment-user-thumbnail
2023년 9월 6일

좋은 글이네요

답글 달기