[Spring Boot] DTO와 엔티티의 매개변수 설계, 왜 이렇게 작성할까?

태수·2026년 7월 21일

웹 구조 탐구

목록 보기
5/5

스프링 부트로 지원 현황 관리 서비스(CareerLog)의 CRUD API를 구현하던 중, GPT가 작성해 준 수정 DTO(ApplicationUpdateRequest)와 엔티티(Application) 예시 코드를 보다가 두 가지 의문이 들었습니다.

// 1. DTO에는 왜 id 필드가 없을까?
public record ApplicationUpdateRequest(
        @NotBlank String companyName,
        @NotBlank String positionTitle,
        String jobUrl,
        @NotNull ApplicationStatus status,
        @NotNull Priority priority,
        LocalDate appliedAt,
        LocalDate resultExpectedAt,
        String nextAction,
        LocalDateTime nextActionAt,
        String memo
) {}

// 2. 엔티티 수정 메서드는 왜 DTO를 안 받고 파라미터 10개를 낱개로 다 풀어서 받을까?
public void update(String companyName, String positionTitle, String jobUrl, ...) {
    this.companyName = companyName;
    this.positionTitle = positionTitle;
    ...
}

"DTO에 id를 넣으면 서비스 메서드에서 DTO 하나만 넘겨받아 더 깔끔해질 텐데?"
"엔티티 수정 메서드도 파라미터 10개를 일일이 받는 것보다 DTO를 덩어리째 넘겨받는 게 훨씬 간결하지 않나?"

그냥 제시해 준 대로 복사해서 쓸 수도 있었지만, 왜 이런 구조가 정석 패턴으로 자리 잡았는지 백엔드 설계 관점에서 파헤쳐 보았습니다.


1. DTO에 ID를 넣지 않는 이유: 데이터 불일치 방지 및 REST 규격

데이터 불일치(Contradiction) 방지

만약 DTO 안에도 id를 넣는다면 API 요청은 다음과 같은 모양이 됩니다.

  • PUT 요청 URL: /api/applications/5 (5번 지원 건 수정 요청)
  • Request Body (DTO): { "id": 10, "companyName": "카카오", ... } (10번 지원 건 데이터)

이처럼 URL 경로의 ID(5)와 Request Body DTO 내부의 ID(10)가 서로 달라지는 데이터 불일치 문제가 발생할 수 있습니다.

이렇게 되면 백엔드 서버 입장에서는 어느 데이터를 기준점으로 삼아야 할지 모호해지고, 컨트롤러나 서비스마다 if (urlId != dto.id()) 같은 중복 검증 로직을 추가해야 하는 번거로움이 생깁니다. DTO에서 ID를 아예 빼버리면 이러한 모호성과 오류 가능성을 근본적으로 차단할 수 있습니다.

RESTful 주소 명시성 및 빠른 권한 검증

예전 글에서 언급했듯 REST API 설계 원칙에서 URL 주소는 제어하려는 자원 그 자체를 의미합니다. PUT /api/applications/5 표현만으로도 5번 자원을 수정하겠다는 목적이 전달됩니다.

또한 서버에서 보안이나 권한 검증(예: "현재 로그인한 유저가 이 5번 지원 건의 작성자가 맞는가?")을 수행할 때, 값비싼 작업인 Request Body(JSON) 파싱을 거치는 것보다 URL 경로의 ID만 빠르게 추출하여 검증하는 것이 성능 및 보안상 훨씬 효율적이라고 합니다.


2. 엔티티 메서드에서 DTO를 받지 않는 이유: 결합도 낮추기

엔티티 내부의 update() 메서드가 DTO를 직접 넘겨받아 파라미터를 줄이면 안 될까 싶은 생각도 들었습니다. 하지만 아키텍처 관점에서 엔티티(Entity)는 DTO의 존재를 몰라야(Import 하지 않아야) 한다고 합니다.

[Controller] ──> [Service] ──> [Entity (핵심)]
      │              │
      └──> [DTO] <───┘
  • 엔티티: DB와 직접 연결된 서비스의 가장 핵심적인 비즈니스 영역
  • DTO: 화면 요구사항에 따라 수시로 변경되는 데이터 전달용 객체

만약 엔티티가 DTO를 직접 의존하게 만들면, 화면이나 DTO 구조가 바뀔 때마다 핵심 도메인인 엔티티 코드까지 함께 영향을 받게 됩니다. 엔티티가 DTO를 모르도록 파라미터를 풀어서 넘겨주는 방식은 외곽의 변화(DTO)가 내부의 핵심(엔티티)을 흔들지 못하도록 보호하는 장치입니다.


3. 서비스 파라미터가 2개인 것은 명확한 역할 분담이다

결과적으로 서비스 단의 update 메서드는 (Long applicationId, ApplicationUpdateRequest request) 2개의 파라미터를 받고, 엔티티 호출 시 DTO의 값을 풀어 전달하게 됩니다.

@Transactional
public ApplicationResponse update(Long applicationId, ApplicationUpdateRequest request) {
    // 1. 식별 키(applicationId)로 수정할 대상을 안전하게 찾음
    Application application = applicationRepository.findById(applicationId)
            .orElseThrow(() -> new IllegalArgumentException("지원 건을 찾을 수 없습니다."));

    // 2. DTO의 데이터들을 풀어 엔티티의 상태를 안전하게 변경
    application.update(
            request.companyName(),
            request.positionTitle(),
            request.jobUrl(),
            ...
    );

    return ApplicationResponse.from(application);
}

언뜻 보면 파라미터가 분리되어 지저분해 보일 수 있지만, 백엔드 관점에서는 "식별 정보"와 "바꿀 내용물"의 역할이 명확히 분리된 깔끔한 코드 흐름입니다.


마무리

AI가 짜주는 예시 코드는 개발 생산성을 압도적으로 올려주지만, 주는 대로 복사해서 쓰기만 해서는 실력이 늘지 않는다고 생각합니다.

  • "왜 굳이 DTO와 URL로 식별자를 분리했을까?"URL과의 데이터 불일치 차단 및 REST 규격 준수
  • "왜 굳이 엔티티에 DTO를 바로 넘기지 않고 파라미터를 풀었을까?"Entity와 DTO 간 단방향 의존성 유지

코드 한 줄, 파라미터 하나에도 "왜?"라는 의문을 갖고 이유를 파헤쳐 보는 과정 속에서 백엔드 아키텍처의 깊은 의도를 배울 수 있었습니다.

0개의 댓글