Home > Troubleshooting > πŸ”[Troubleshooting] πŸš€ @NoArgsConstructor μ‚¬μš©: DTO vs Entity

πŸ”[Troubleshooting] πŸš€ @NoArgsConstructor μ‚¬μš©: DTO vs Entity
Troubleshooting Backend Development Spring Boot

πŸš€ @NoArgsConstructor μ‚¬μš©: DTO vs Entity

πŸ”‘ 핡심 차이: μƒμ„±μžμ˜ μ ‘κ·Ό μ œμ–΄μž

κ²°λ‘ λΆ€ν„° λ§μ”€λ“œλ¦¬λ©΄, 두 μ–΄λ…Έν…Œμ΄μ…˜μ˜ μœ μΌν•œ μ°¨μ΄λŠ” μƒμ„±λ˜λŠ” κΈ°λ³Έ μƒμ„±μž(no-argument constructor)의 μ ‘κ·Ό μ œμ–΄μž(Access Modifier) μž…λ‹ˆλ‹€.

@NoArgsConstructor

public κΈ°λ³Έ μƒμ„±μžλ₯Ό λ§Œλ“­λ‹ˆλ‹€.

public class MyDto {
    // @NoArgsConstructorκ°€ μƒμ„±ν•œ μ½”λ“œ
    public MyDto() {
    }
}

@NoArgsConstructor(access = AccessLevel.PROTECTED)

protected κΈ°λ³Έ μƒμ„±μžλ₯Ό λ§Œλ“­λ‹ˆλ‹€.

public class MyEntity {
    // @NoArgsConstructor(access = AccessLevel.PROTECTED)κ°€ μƒμ„±ν•œ μ½”λ“œ
    protected MyEntity() {
    }
}

πŸ€” μ™œ 이 차이가 μ€‘μš”ν•œκ°€μš”?

이 μ ‘κ·Ό μ œμ–΄μžμ˜ μ°¨μ΄λŠ” β€œλˆ„κ°€ 이 클래슀λ₯Ό 직접 생성할 수 μžˆλŠ”κ°€?” λ₯Ό κ²°μ •ν•˜λ©°, μ΄λŠ” JPA Entity와 DTOμ—μ„œ 각각 λ‹€λ₯Έ 의미λ₯Ό κ°€μ§‘λ‹ˆλ‹€.


πŸ“₯ Request DTOμ—μ„œ: @NoArgsConstructor (public)

μ™œ public κΈ°λ³Έ μƒμ„±μžκ°€ ν•„μš”ν•œκ°€μš”?

Request DTOλŠ” 주둜 μ™ΈλΆ€(ν΄λΌμ΄μ–ΈνŠΈ)μ—μ„œ 온 JSON 데이터λ₯Ό μžλ°” 객체둜 λ³€ν™˜(Deserialization)ν•  λ•Œ μ‚¬μš©λ©λ‹ˆλ‹€.

Spring Bootκ°€ ν΄λΌμ΄μ–ΈνŠΈλ‘œλΆ€ν„° 받은 JSON 데이터λ₯Ό DTO 객체둜 λ³€ν™˜ν•˜λŠ” κ³Όμ • λ•Œλ¬Έμž…λ‹ˆλ‹€.

JSON β†’ DTO λ³€ν™˜ κ³Όμ •

  1. 객체 생성 단계
    • ν΄λΌμ΄μ–ΈνŠΈκ°€ APIλ₯Ό ν˜ΈμΆœν•˜λ©΄, Spring은 λ‚΄λΆ€μ μœΌλ‘œ Jackson 라이브러리λ₯Ό μ‚¬μš©ν•΄ HTTP Body의 JSON λ¬Έμžμ—΄μ„ μ½μŠ΅λ‹ˆλ‹€
    • Jackson은 이 JSON 데이터λ₯Ό 담을 μžλ°” 객체(Request DTO)λ₯Ό λ¨Όμ € 생성해야 ν•©λ‹ˆλ‹€
  2. public κΈ°λ³Έ μƒμ„±μž 호좜
    • Jackson은 κ°€μž₯ κ°„λ‹¨ν•˜κ³  ν‘œμ€€μ μΈ 방법인 public κΈ°λ³Έ μƒμ„±μž(new YourRequestDto()) λ₯Ό ν˜ΈμΆœν•˜μ—¬ 일단 ν…… 빈 DTO 객체λ₯Ό λ§Œλ“­λ‹ˆλ‹€
  3. ν•„λ“œ κ°’ μ£Όμž…
    • κ·Έ 후에 JSON의 각 ν•„λ“œ("chapterName": "μ„ μ‚¬μ‹œλŒ€")λ₯Ό λΆ„μ„ν•˜μ—¬, μƒμ„±λœ DTO 객체의 ν•΄λ‹Ή ν•„λ“œμ— 값을 μ„Έν„°(setter)λ‚˜ λ¦¬ν”Œλ ‰μ…˜(reflection)을 톡해 μ£Όμž…ν•©λ‹ˆλ‹€

⚠️ λ§Œμ•½ public κΈ°λ³Έ μƒμ„±μžκ°€ μ—†λ‹€λ©΄ Jackson은 1λ‹¨κ³„μ—μ„œ 객체λ₯Ό μƒμ„±ν•˜λŠ” 것뢀터 μ‹€νŒ¨ν•˜κ²Œ λ˜μ–΄ 였λ₯˜κ°€ λ°œμƒν•©λ‹ˆλ‹€.

μ½”λ“œ μ˜ˆμ‹œ

package com.kobe.koreahistory.dto.request;

import lombok.Getter;
import lombok.NoArgsConstructor;

/**
 * 챕터 생성 μš”μ²­ DTO
 */
@Getter
@NoArgsConstructor  // public κΈ°λ³Έ μƒμ„±μžλ₯Ό λ§Œλ“€μ–΄μ€Œ
public class ChapterCreateRequestDto {
    private int chapterNumber;
    private String chapterTitle;
    // ... ν•„λ“œλ“€
}

핡심 포인트

Request DTOμ—λŠ” @NoArgsConstructorλ₯Ό μ‚¬μš©ν•˜μ—¬ public κΈ°λ³Έ μƒμ„±μžλ₯Ό μ—΄μ–΄λ‘λŠ” 것이 일반적이고 μ˜¬λ°”λ₯Έ λ°©λ²•μž…λ‹ˆλ‹€.


πŸ—„οΈ JPA Entityμ—μ„œ: @NoArgsConstructor(access = AccessLevel.PROTECTED)

μ™œ protectedλ₯Ό μ‚¬μš©ν•˜λ‚˜μš”?

JPA EntityλŠ” λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ”κ³Ό 직접 λ§€ν•‘λ˜λŠ” 핡심 도메인 κ°μ²΄μž…λ‹ˆλ‹€. 이 κ°μ²΄λŠ” ν•¨λΆ€λ‘œ μƒμ„±λ˜μ–΄μ„œλŠ” μ•ˆ 되며, 항상 μΌκ΄€λœ μƒνƒœλ₯Ό μœ μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

JPAκ°€ κΈ°λ³Έ μƒμ„±μžλ₯Ό ν•„μš”λ‘œ ν•˜λŠ” 이유

  • JPA λͺ…μ„Έ: JPAλŠ” DBμ—μ„œ 데이터λ₯Ό μ‘°νšŒν•˜μ—¬ Entity 객체λ₯Ό λ§Œλ“€ λ•Œ, λ‚΄λΆ€μ μœΌλ‘œ λ¦¬ν”Œλ ‰μ…˜(reflection)을 톡해 κΈ°λ³Έ μƒμ„±μžλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€
  • λ”°λΌμ„œ κΈ°λ³Έ μƒμ„±μžλŠ” λ°˜λ“œμ‹œ ν•„μš”ν•©λ‹ˆλ‹€

μ™œ public이 μ•„λ‹Œ protected인가?

λ§Œμ•½ κΈ°λ³Έ μƒμ„±μžλ₯Ό public으둜 열어두면, κ°œλ°œμžκ°€ μ„œλΉ„μŠ€ 둜직 λ“±μ—μ„œ 아무 생각 없이 new Chapter()와 같이 λΉ„μ–΄μžˆλŠ”(μƒνƒœκ°€ λΆˆμ™„μ „ν•œ) Entity 객체λ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ΄λŠ” 데이터 무결성을 ν•΄μΉ˜κ³  버그λ₯Ό μœ λ°œν•˜λŠ” μ£Όμš” 원인이 λ©λ‹ˆλ‹€.

protected의 μ•ˆμ „μž₯치 μ—­ν• 

μƒμ„±μžλ₯Ό protected 둜 λ§Œλ“€λ©΄, μ™ΈλΆ€ νŒ¨ν‚€μ§€μ—μ„œ new Chapter()λ₯Ό ν˜ΈμΆœν•˜λŠ” 것을 막을 수 μžˆμŠ΅λ‹ˆλ‹€.

μ΄λŠ” κ°œλ°œμžμ—κ²Œ β€œμ΄ κ°μ²΄λŠ” λΉŒλ”(@Builder)λ‚˜ 정적 νŒ©ν† λ¦¬ λ©”μ„œλ“œμ²˜λŸΌ μ •ν•΄μ§„ λ°©μ‹μœΌλ‘œλ§Œ 생성해야 ν•΄!β€λΌλŠ” λͺ…ν™•ν•œ λ©”μ‹œμ§€λ₯Ό μ „λ‹¬ν•˜λŠ” μ•ˆμ „μž₯치 역할을 ν•©λ‹ˆλ‹€.

πŸ’‘ JPAλŠ” λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•˜λ―€λ‘œ protected여도 λ¬Έμ œμ—†μ΄ 객체λ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ½”λ“œ μ˜ˆμ‹œ

package com.kobe.koreahistory.entity;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AccessLevel;

import javax.persistence.*;

/**
 * 챕터 μ—”ν‹°ν‹°
 */
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // protected κΈ°λ³Έ μƒμ„±μž
public class Chapter {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private int chapterNumber;
    private String chapterTitle;
    
    @Builder
    public Chapter(int chapterNumber, String chapterTitle) {
        this.chapterNumber = chapterNumber;
        this.chapterTitle = chapterTitle;
    }
}

πŸ“Š μ΅œμ’… 정리

Β  @NoArgsConstructor
(public)
@NoArgsConstructor
(access = AccessLevel.PROTECTED)
μƒμ„±μž public protected
μ£Όμš” μš©λ„ Request DTO
(JSON μ—­μ§λ ¬ν™”μš©)
JPA Entity
(객체 μƒμ„±μ˜ μ•ˆμ •μ„± ν™•λ³΄μš©)
μ˜λ„ β€œμ΄ ν΄λž˜μŠ€λŠ” μ™ΈλΆ€μ—μ„œ
자유둭게 생성될 수 μžˆμŠ΅λ‹ˆλ‹€β€
β€œμ΄ ν΄λž˜μŠ€λŠ” μ •ν•΄μ§„ 방식 μ™Έμ—λŠ”
ν•¨λΆ€λ‘œ μƒμ„±ν•˜μ§€ λ§ˆμ„Έμš”β€
λΉ„μœ  문을 ν™œμ§ 열어두기 πŸšͺ 문을 μ‚΄μ§λ§Œ 열어두기 πŸ”’

πŸ’‘ 핡심 원칙

Request DTO

μ™ΈλΆ€μ˜ JSON 데이터λ₯Ό λ°›μ•„μ•Ό ν•˜λ―€λ‘œ, λˆ„κ΅¬λ‚˜ 객체λ₯Ό 생성할 수 μžˆλ„λ‘ 문을 ν™œμ§ 열어두어야 ν•©λ‹ˆλ‹€.

πŸ‘‰ @NoArgsConstructor (public)

JPA Entity

핡심 도메인 κ°μ²΄μ΄λ―€λ‘œ, μ •ν•΄μ§„ κ·œμΉ™(λΉŒλ” νŒ¨ν„΄ λ“±)으둜만 μƒμ„±ν•˜λ„λ‘ κ°•μ œν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

πŸ‘‰ @NoArgsConstructor(access = AccessLevel.PROTECTED)

이 원칙을 μ§€ν‚€λ©΄ 더 μ•ˆμ •μ μ΄κ³  μ˜λ„κ°€ λͺ…ν™•ν•œ μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.