Home > Troubleshooting > πŸ”[Troubleshooting] πŸš€ DTO 섀계

πŸ”[Troubleshooting] πŸš€ DTO 섀계
Troubleshooting Backend Development Spring Boot

πŸš€ DTO 섀계!

🎯 핡심 이슈

생성(Create) μ‹œ μ‚¬μš©ν•˜λ˜ StudentRequestDtoλ₯Ό μˆ˜μ •(Update) APIμ—μ„œ μž¬μ‚¬μš©ν•˜κ³  μžˆμ–΄, API μ‚¬μš©μžμ—κ²Œ ν˜Όλž€μ„ 쀄 수 μžˆμŠ΅λ‹ˆλ‹€.


⚠️ ν˜„μž¬ μ½”λ“œμ˜ 문제점

1. API μ˜λ„κ°€ 뢈λͺ…ν™•

// StudentRequestDtoλŠ” μ—¬λŸ¬ ν•„λ“œλ₯Ό 포함
- name
- admissionYear
- majorName
- doubleMajorName

API μ‚¬μš©μžμ˜ ν˜Όλž€

  • β€œμ΄λ¦„λ§Œ 보내면 λ˜λ‚˜?”
  • β€œλ‹€λ₯Έ ν•„λ“œλ“€λ„ μ±„μ›Œμ•Ό ν•˜λ‚˜?”
  • β€œnull둜 보내면 μ–΄λ–»κ²Œ λ˜μ§€?”

2. λΆˆν•„μš”ν•œ 데이터 전솑

ν΄λΌμ΄μ–ΈνŠΈκ°€ μ΄λ¦„λ§Œ μˆ˜μ •ν•˜κ³  싢어도, λ‹€λ₯Έ ν•„λ“œλ“€μ„ JSON에 포함할 수 μžˆμŠ΅λ‹ˆλ‹€.

// λΆˆν•„μš”ν•˜κ²Œ λ³΅μž‘ν•œ μš”μ²­
{
  "name": "홍길동",
  "admissionYear": 2024,    // λΆˆν•„μš”
  "majorName": "컴퓨터곡학",   // λΆˆν•„μš”
  "doubleMajorName": null   // λΆˆν•„μš”
}

3. μœ μ§€λ³΄μˆ˜ 리슀크

StudentRequestDto에 μƒˆλ‘œμš΄ ν•„μˆ˜ ν•„λ“œκ°€ μΆ”κ°€λ˜λ©΄?

public class StudentRequestDto {
    @NotNull  // μƒˆλ‘œμš΄ ν•„μˆ˜ ν•„λ“œ μΆ”κ°€
    private String phoneNumber;
    
    // κΈ°μ‘΄ ν•„λ“œλ“€...
}

β†’ μ΄λ¦„λ§Œ μˆ˜μ •ν•˜λŠ” APIκ°€ μ˜λ„μΉ˜ μ•Šκ²Œ 영ν–₯을 λ°›μ•„ 였λ₯˜ λ°œμƒ!


βœ… ν•΄κ²° λ°©μ•ˆ: λͺ©μ λ³„ DTO 뢄리

섀계 원칙

ν•˜λ‚˜μ˜ APIλŠ” ν•˜λ‚˜μ˜ λͺ…ν™•ν•œ 계약(Contract)을 κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€.

β€œν•™μƒ 이름 μˆ˜μ •β€μ΄λΌλŠ” 단일 λͺ©μ μ— λ§žλŠ” μ „μš© DTOλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.


πŸ”§ κ΅¬ν˜„ κ°€μ΄λ“œ

Step 1: μ „μš© DTO 생성

UpdateStudentNameRequestDto.java (μ‹ κ·œ 생성)

package com.kobe.schoolmanagement.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor  // JSON 역직렬화λ₯Ό μœ„ν•œ κΈ°λ³Έ μƒμ„±μž
public class UpdateStudentNameRequestDto {

    @NotBlank(message = "이름은 λΉ„μ›Œλ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€.")
    private String name;
}

μ£Όμš” νŠΉμ§•

  • βœ… ν•„μš”ν•œ ν•„λ“œλ§Œ 포함 (name 단일 ν•„λ“œ)
  • βœ… μœ νš¨μ„± 검증 κ·œμΉ™ λͺ…ν™• (@NotBlank)
  • βœ… λͺ©μ μ΄ λͺ…ν™• (이름 μˆ˜μ • μ „μš©)

Step 2: Controller μˆ˜μ •

Before (ν˜„μž¬)

@PatchMapping("/change/student/name/{studentId}")
public ResponseEntity<StudentResponseDto> changeStudentName(
    @PathVariable String studentId,
    @RequestBody StudentRequestDto requestDto  // λ²”μš© DTO μ‚¬μš©
) {
    return ResponseEntity.ok(studentService.updateStudentName(studentId, requestDto));
}

After (κ°œμ„ )

import com.kobe.schoolmanagement.dto.request.UpdateStudentNameRequestDto;

@PatchMapping("/change/student/name/{studentId}")
public ResponseEntity<StudentResponseDto> changeStudentName(
    @PathVariable String studentId,
    @RequestBody @Valid UpdateStudentNameRequestDto requestDto  // μ „μš© DTO + 검증
) {
    return ResponseEntity.ok(studentService.updateStudentName(studentId, requestDto));
}

λ³€κ²½ 포인트

  • StudentRequestDto β†’ UpdateStudentNameRequestDto
  • @Valid μ–΄λ…Έν…Œμ΄μ…˜ μΆ”κ°€λ‘œ μœ νš¨μ„± 검증 ν™œμ„±ν™”

Step 3: Service μˆ˜μ •

Before (ν˜„μž¬)

@Transactional
public StudentResponseDto updateStudentName(String studentId, StudentRequestDto requestDto) {
    Student findStudent = studentRepository.findByStudentId(studentId)
        .orElseThrow(() -> new IllegalArgumentException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν•™μƒμž…λ‹ˆλ‹€."));

    findStudent.updateStudentName(requestDto.getName());

    return StudentResponseDto.fromEntity(findStudent);
}

After (κ°œμ„ )

import com.kobe.schoolmanagement.dto.request.UpdateStudentNameRequestDto;

@Transactional
public StudentResponseDto updateStudentName(String studentId, UpdateStudentNameRequestDto requestDto) {
    Student findStudent = studentRepository.findByStudentId(studentId)
        .orElseThrow(() -> new IllegalArgumentException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν•™μƒμž…λ‹ˆλ‹€."));

    findStudent.updateStudentName(requestDto.getName());

    return StudentResponseDto.fromEntity(findStudent);
}

λ³€κ²½ 포인트

  • νŒŒλΌλ―Έν„° νƒ€μž…λ§Œ UpdateStudentNameRequestDto둜 λ³€κ²½
  • λ‚˜λ¨Έμ§€ λ‘œμ§μ€ 동일

πŸ“Š κ°œμ„  효과 비ꡐ

ν•­λͺ© Before After
API λͺ…ν™•μ„± ❌ μ–΄λ–€ ν•„λ“œκ°€ ν•„μš”ν•œμ§€ 뢈λͺ…ν™• βœ… name만 ν•„μš”ν•¨μ„ λͺ…ν™•νžˆ ν‘œν˜„
데이터 전솑 ❌ λΆˆν•„μš”ν•œ ν•„λ“œ 포함 κ°€λŠ₯ βœ… ν•„μš”ν•œ λ°μ΄ν„°λ§Œ 전솑
μœ μ§€λ³΄μˆ˜ ❌ λ‹€λ₯Έ API 변경에 영ν–₯λ°›μŒ βœ… λ…λ¦½μ μœΌλ‘œ 관리 κ°€λŠ₯
μœ νš¨μ„± 검증 ⚠️ 암묡적 βœ… λͺ…μ‹œμ  (@NotBlank)

πŸ’‘ API μš”μ²­ μ˜ˆμ‹œ

κ°œμ„  ν›„ ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­

{
  "name": "홍길동"
}

μž₯점

  • κ°„κ²°ν•˜κ³  λͺ…ν™•ν•œ μš”μ²­ ꡬ쑰
  • API μ˜λ„κ°€ ν•œλˆˆμ— νŒŒμ•…λ¨
  • λΆˆν•„μš”ν•œ ν•„λ“œ 전솑 제거

πŸ“ 섀계 원칙 정리

DTO 섀계 μ‹œ 고렀사항

  1. 단일 μ±…μž„ 원칙
    • ν•˜λ‚˜μ˜ DTOλŠ” ν•˜λ‚˜μ˜ λͺ…ν™•ν•œ λͺ©μ μ„ κ°€μ Έμ•Ό 함
  2. λͺ…μ‹œμ  계약
    • API μ‚¬μš©μžκ°€ μ–΄λ–€ 데이터λ₯Ό 보내야 ν•˜λŠ”μ§€ λͺ…ν™•νžˆ μ•Œ 수 μžˆμ–΄μ•Ό 함
  3. 독립성
    • λ‹€λ₯Έ API의 변경이 영ν–₯을 μ£Όμ§€ μ•Šλ„λ‘ λ…λ¦½μ μœΌλ‘œ 관리
  4. μœ νš¨μ„± 검증
    • DTO λ ˆλ²¨μ—μ„œ λͺ…μ‹œμ μœΌλ‘œ 검증 κ·œμΉ™ μ •μ˜

🎯 결둠

λͺ©μ μ— λ§žλŠ” μ „μš© DTOλ₯Ό μ‚¬μš©ν•˜λ©΄:

  • API 계약이 λͺ…ν™•ν•΄μ§‘λ‹ˆλ‹€
  • λ‹€λ₯Έ 개발자의 ν˜Όλž€μ„ λ°©μ§€ν•©λ‹ˆλ‹€
  • μ•ˆμ „ν•˜κ³  μœ μ§€λ³΄μˆ˜ν•˜κΈ° 쒋은 μ½”λ“œκ°€ λ©λ‹ˆλ‹€

μ΄λŠ” λ‹¨μˆœνžˆ μ½”λ“œλ₯Ό 더 μ“°λŠ” 것이 μ•„λ‹ˆλΌ, 더 λ‚˜μ€ 섀계λ₯Ό ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. πŸ‘