❓ ChatGPT에 질문
package jpabook.jpashop.controller;
import jakarta.validation.Valid;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
if (result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
}
package jpabook.jpashop.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
@Enumerated(EnumType.STRING)
private Address address;
@OneToMany(mappedBy = "member")
private List orders = new ArrayList<>();
}
package jpabook.jpashop.controller;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
이렇게 Member Entity가 있는데 createMember할 때 MemberForm을 따로 만들어서 넘겨주는 이유는?
📝 답변
1. 엔티티와 뷰 간의 결합을 느슨하게 유지
엔티티(Member
)는 주로 데이터베이스와 직접 매핑되며, 비즈니스 로직을 표현합니다. 만약 엔티티를 그대로 폼 데이터와 연관 지으면, 뷰에 필요한 추가 정보나 검증 로직이 엔티티에 추가되어야 합니다.
이렇게 되면 엔티티가 컨트롤러나 뷰의 요구사항에 종속되어 애플리케이션 설계가 단단히 결합됩니다. DTO(MemberForm
)를 사용하면, 뷰와 관련된 로직이나 검증 규칙은 DTO에 두고, 엔티티는 비즈니스 로직에 집중할 수 있습니다.
2. 유효성 검증의 분리
@Valid
와 같은 검증 어노테이션은 주로 폼 입력 데이터를 검증하는 데 사용됩니다. 엔티티에 직접 @NotEmpty
같은 검증 어노테이션을 추가하면, 이는 폼 데이터뿐만 아니라 데이터베이스와 연관된 로직에도 영향을 미칩니다.
예를 들어, 데이터베이스에서 데이터를 읽어올 때도 유효성 검사가 실행될 수 있습니다. DTO를 사용하면 검증 로직을 엔티티와 분리하여, 검증과 데이터 저장의 책임을 명확히 분리할 수 있습니다.
3. 엔티티의 보안 문제
엔티티는 데이터베이스와 1:1로 매핑되기 때문에 민감한 정보(예: 사용자 비밀번호, ID, 상태 값 등)가 포함될 수 있습니다. 폼 데이터로 엔티티를 직접 노출하면, 이러한 민감한 정보가 클라이언트로 노출될 위험이 있습니다. DTO를 사용하면 클라이언트에 필요한 정보만 노출하고, 민감한 정보를 보호할 수 있습니다.
4. 입력 데이터와 엔티티의 구조가 다른 경우
클라이언트에서 입력받은 데이터가 엔티티와 동일한 구조를 갖지 않을 때, DTO를 사용하면 유연하게 처리할 수 있습니다.
예: 폼 입력에는 비밀번호 확인 필드(passwordConfirm
)가 있을 수 있지만, 엔티티에는 해당 필드가 필요하지 않습니다. 이처럼 폼 데이터와 엔티티가 서로 다른 경우, DTO를 통해 데이터를 변환할 수 있습니다.
5. 테스트 및 유지보수 용이성
엔티티와 뷰를 분리하면 각각 독립적으로 테스트할 수 있습니다. DTO를 사용하면, 애플리케이션의 유지보수성이 향상됩니다. 예: 뷰의 요구사항이 변경되어도 DTO만 수정하면 되고, 엔티티는 그대로 유지할 수 있습니다.
6. 변경 요청 대응
엔티티 구조는 주로 데이터베이스 스키마와 연관되어 변경하기 어렵습니다. 반면, DTO는 클라이언트 요구사항에 맞게 자유롭게 설계하고 수정할 수 있습니다.
결론
MemberForm
을 사용하는 이유는 다음과 같습니다:
- 엔티티와 뷰 간 결합을 방지하여 코드 설계를 느슨하게 유지.
- 유효성 검증과 비즈니스 로직을 분리.
- 보안 문제를 예방.
- 입력 데이터와 엔티티 구조의 차이를 유연하게 처리.
- 테스트와 유지보수를 용이하게 만듦.
즉, MemberForm
은 엔티티와 뷰 사이의 안전한 중간 다리 역할을 하며, 애플리케이션 설계를 더 유연하고 견고하게 만들어줍니다. 😊
📝 예시
@GetMapping("/members")
public String members(Model model) {
List<Member> members = memberService.findAll();
// 화면에 전달할 때는 MemberForm을 전달해야지 엔티티(Member)를 전달하는 것은 좋지 않다.
// 특히 API끼리 통신할 때 만약 엔티티의 속성이 변경되면 API 스펙이 변해야 한다.
// 지금은 서버사이드렌더링이기 때문에 괜찮음
model.addAttribute("members", members);
return "members/memberList";
}
엔티티(Member)를 직접 전달하는 것이 왜 좋지 않은가?
1. 왜 엔티티를 화면에 바로 전달하는 것이 좋지 않을까?
엔티티는 데이터베이스와 직접 매핑되는 객체로, 비즈니스 로직과 데이터 모델의 중심 역할을 합니다. 이를 화면이나 API 응답에 바로 사용하면 여러 가지 문제가 발생할 수 있습니다.
1-1. 엔티티 구조 변경의 영향
만약 엔티티의 속성이 변경되면, 이를 사용하는 모든 화면이나 API가 영향을 받습니다.
@Entity
public class Member {
private String name;
private String address;
}
예를 들어, Member
엔티티에 email
속성을 추가하거나 기존 필드를 삭제한다면, 이 엔티티를 사용하던 화면이나 API도 수정해야 합니다. 뷰와 엔티티가 강하게 결합되어 있기 때문에 유지보수가 어려워집니다.
1-2. 보안 문제
엔티티에는 민감한 정보(예: 비밀번호, 권한, 개인 식별 정보 등)가 포함될 수 있습니다. 엔티티를 그대로 화면이나 API에 노출하면 이러한 데이터가 그대로 외부에 전달될 위험이 있습니다.
@Entity
public class Member {
private String name;
private String address;
private String password; // 화면에 노출되면 안 되는 데이터
}
이런 상황에서는 민감한 데이터를 숨기기 위해 DTO를 사용하거나 필요한 데이터만 골라서 전달해야 합니다.
1-3. API 스펙의 안정성
특히 API와 통신할 때 엔티티를 그대로 사용하는 것은 큰 문제가 될 수 있습니다.
- API의 응답 스펙은 한 번 공개되면 안정적으로 유지되어야 하지만, 엔티티의 구조는 비즈니스 요구사항에 따라 자주 변경될 수 있습니다.
- 엔티티 변경 → API 응답 변경 → 클라이언트 코드 수정이 필요하므로 API 소비자에게 불편을 초래하게 됩니다.
2. 왜 서버사이드 렌더링에서는 괜찮다고 할까?
주석에 언급된 것처럼, 지금 작성된 코드는 서버사이드 렌더링(Server-side Rendering)을 사용하고 있습니다.
서버사이드 렌더링이란?
서버에서 HTML을 생성하여 클라이언트(브라우저)에 전달하는 방식입니다. 이 경우, 데이터가 서버에서만 처리되고 클라이언트는 HTML만 받아 보기 때문에, 엔티티를 뷰로 직접 전달해도 큰 문제가 발생하지 않습니다.
이유:
- 데이터가 클라이언트에 노출되는 방식이 제한적입니다.
- 데이터가 API로 외부에 노출되는 것이 아니라, 서버에서 렌더링된 HTML만 브라우저에 전달됩니다.
- 엔티티를 직접 사용해도 구조 변경이 클라이언트 코드에 큰 영향을 미치지 않습니다.
3. DTO를 사용하는 것이 더 좋은 이유
비록 서버사이드 렌더링이라도, 유지보수성과 설계의 일관성을 위해 엔티티 대신 DTO를 사용하는 것이 더 좋은 설계입니다.
장점:
- 뷰와 엔티티의 독립성 유지: 엔티티 구조가 변경되더라도 뷰에 영향을 주지 않음.
- 보안 강화: 민감한 데이터를 노출하지 않음.
- 데이터 전환 용이: 화면에 필요한 데이터만 전달하거나 변환 가능.
예시: MemberForm을 사용하는 방식
// MemberForm DTO
public class MemberForm {
private String name;
private String city;
private String street;
}
컨트롤러에서 데이터를 변환하여 뷰에 전달합니다:
@GetMapping("/members")
public String members(Model model) {
List<Member> members = memberService.findAll();
// 엔티티를 DTO로 변환
List<MemberForm> memberForms = members.stream()
.map(member -> {
MemberForm form = new MemberForm();
form.setName(member.getName());
form.setCity(member.getAddress().getCity());
form.setStreet(member.getAddress().getStreet());
return form;
})
.collect(Collectors.toList());
model.addAttribute("members", memberForms);
return "members/memberList";
}
4. 결론
주석의 핵심 요점:
- 엔티티를 직접 화면이나 API에 전달하면 구조 변경, 보안 문제, API 스펙 불안정성이 발생할 수 있습니다.
- 지금은 서버사이드 렌더링이라 큰 문제가 없지만, API 통신이나 유지보수를 고려하면 DTO를 사용하는 것이 더 좋은 설계입니다.
항상 좋은 설계는 미래의 변경에 유연하게 대처할 수 있어야 합니다! 😊
'spring' 카테고리의 다른 글
HandlerMapping & RequestMapping (0) | 2024.12.02 |
---|---|
SpringMVC ArgumentResolver (0) | 2024.12.02 |
@RequestBody @Valid Object obj, BindingResult br 타입에러가 br에 담기지 않는 이유 (0) | 2024.11.30 |
도메인 모델 패턴 vs 트랜잭션 스크립트 패턴 (0) | 2024.11.25 |
자바 접근제어자 개념과 JPA @Embeddable에서 기본생성자에 proteced를 사용하는 이유 (0) | 2024.11.24 |
댓글