소스 분석을 하면서 보기 어려웠던 것은 전달받는 데이터를
request.getParameter로 하나하나 코딩한 부분이다.
심한 건 메서드 하나에 파라미터로 받고 전달하는 코드만 몇십 줄이 넘는다.
디버거로 값을 일일히 확인하는 것도 쉽지 않았고 비효율적이라 생각.
form에서 받은 값들을 Dto에 담는다면 코드량은 당연히 줄 것이고
Dto 클래스명만 봐도 어떤 데이터가 전송, 전달되는 지 쉽게 확인이 가능할 것...
이라고 말은 했지만 소스가 너무 방대하고 그것을 리팩토링할 실력은 없다..
여기서 내가 가져가야 할 것은
Dto가 무엇이고
왜 사용해야하는 지,
사용했을 때의 장점,
그리고 간단한 예제를 만들어봄으로써 Dto 사용 습관이다.
DTO란?
- Domain Transfer Object.
- 데이터 전송 객체로, 주로 SW 시스템의 계층 간 데이터 교환을 위해 사용.
- 클라이언트와 서버 간의 데이터 전송
- 서비스 계층과 데이터 액세스 계층 간의 데이터 전송
사용 이유
- 데이터 전송 및 교화의 효율성
- Dto를 사용하면 필요한 데이터만 전송하고, 데이터 전송 과정에서 필요없는 정보를 제거함으로써
효율적인 데이터 교환이 가능.
- Dto를 사용하면 필요한 데이터만 전송하고, 데이터 전송 과정에서 필요없는 정보를 제거함으로써
- 데이터 구조의 표준화
- 여러 계층에서 동일한 데이터 구조를 사용함으로써, 데이터의 일관성과 일치성을 유지.
- 클라이언트와 서버 간의 데이터 모델을 분리하여 독립적을 개선 및 변경 가능.
- 보안과 데이터 은닉
- Dto는 데이터의 특정 부분만 전송하므로, 중요한 정보를 숨기고 보안을 강화할 수 있음.
- 필요한 데이터만 Dto에 포함시키고, 민감한 정보를 제회함으로써 데이터 은닉성 유지.
- 클라이어트와 서버의 종속성 감소
- 클라이언트는 서버로부터 필요한 데이터만 받아오고, 서버는 클라이언트에 필요한 데이터만 제공함으로써,
서로의 독립성과 유연성을 향상
- 클라이언트는 서버로부터 필요한 데이터만 받아오고, 서버는 클라이언트에 필요한 데이터만 제공함으로써,
- 객체 간 데이터 전달의 편의성
- Dto는 여러 개의 속성을 하나의 객체로 묶어 관리할 수 있으며, 객체 간의 데이터 전달을 편리하게 해줌.
- 복잡한 데이터 구조를 캡슐화하고, 데이터 일관성 유지.
DB는 따로 사용하지 않고 메모리에만 저장되게 직접 구현할 예정.
Post
@Getter
@NoArgsConstructor
public class Post {
private Long id;
private String title;
private String author;
private String content;
@Builder
public Post(Long id, String title, String author, String content) {
this.id = id;
this.title = title;
this.author = author;
this.content = content;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
@Setter 대신 @Builder를 넣은 이유는 가독성 때문이다.
//@Setter 사용시 코드
Post post = new Post();
post.setTitle("test");
post.setAuthor("tester");
post.setContent("test content");
//@Builder 사용시 코드
Post post = Post.build
.title("test")
.author("tester")
.content("test content")
.build();
Post클래스의 저장,조회,수정 Dto를 만들어보자
PostSaveRequestDto
@Getter
@NoArgsConstructor
@ToString
public class PostSaveRequestDto {
private String title;
private String author;
private String content;
@Builder
public PostSaveRequestDto(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
}
}
PostResponseDto - findById/findAll
@Getter
@ToString
public class PostResponseDto {
private Long id;
private String title;
private String author;
private String content;
public PostResponseDto(Post entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.content = entity.getContent();
}
}
PostUpdateRequestDto
@Getter
@NoArgsConstructor
public class PostUpdateRequestDto {
private String title;
private String content;
@Builder
public PostUpdateRequestDto(String title, String content) {
this.title = title;
this.content = content;
}
}
Dto마다 @Builder를 설정해주었고
@ToString도 추가했다.
@ToString을 추가한 이유는 log 찍을 때 들어오는 값 확인하기 위함.
Dto를 파라미터로 받고
데이터가 저장될 PostRepository
PostRepository
@Repository
public class PostRepository {
private static final Map<Long, Post> store = new HashMap<>();
private static long sequence = 0L;
public Post save(PostSaveRequestDto saveDto) {
Post post = Post.builder()
.id(++sequence)
.title(saveDto.getTitle())
.author(saveDto.getAuthor())
.content(saveDto.getContent())
.build();
store.put(post.getId(), post);
return post;
}
public Post findById(Long id) {
return store.get(id);
}
public List<Post> findAll() {
return new ArrayList<>(store.values());
}
public Post update(Long id, PostUpdateRequestDto updateDto) {
Post findPost = findById(id);
findPost.update(updateDto.getTitle(), updateDto.getContent());
return findPost;
}
public void delete(Long id) {
store.remove(id);
}
}
PostService
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
public String save(PostSaveRequestDto requestDto) {
postRepository.save(requestDto);
return "저장 완료";
}
public PostResponseDto findById(Long id) {
Post entity = postRepository.findById(id);
return new PostResponseDto(entity);
}
public List<PostResponseDto> findAll() {
return postRepository.findAll().stream()
.map(posts -> new PostResponseDto(posts))
.collect(Collectors.toList());
}
public String update(Long id, PostUpdateRequestDto requestDto) {
postRepository.update(id, requestDto);
return "수정 완료";
}
public String delete(Long id) {
postRepository.delete(id);
return "삭제 완료";
}
}
findAll()은 리턴 타입이 Post이기 때문에 PostListResponseDto로 변환필요.
현재 스트림을 사용했지만 풀어서 표현한다면
List<PostResponseDto> postList = new ArrayList<>();
for (Post result : postRepository.findAll()) {
postList.add(new PostResponseDto(result));
}
return postList;
화면 구현없이 API로 진행.
PostController
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/post")
public class PostController {
private final PostService postService;
@GetMapping("/{id}")
public ResponseEntity<PostResponseDto> findById(@PathVariable Long id) {
PostResponseDto dto = postService.findById(id);
log.info("post: {}", dto);
return ResponseEntity.ok().body(dto);
}
@PostMapping("/")
public ResponseEntity<String> save(@RequestBody PostSaveRequestDto requestDto) {
String savePost = postService.save(requestDto);
log.info("savePost: {}", requestDto);
return ResponseEntity.ok().body(savePost);
}
@GetMapping("/all")
public ResponseEntity<List<PostListResponseDto>> findAll() {
List<PostListResponseDto> dto = postService.findAll();
log.info("all : {}", dto);
return ResponseEntity.ok().body(dto);
}
@PatchMapping("/{id}")
public ResponseEntity<String> update(@PathVariable Long id, @RequestBody PostUpdateRequestDto requestDto) {
String updatePost = postService.update(id, requestDto);
log.info("update : {}", requestDto);
return ResponseEntity.ok().body(updatePost);
}
@DeleteMapping("/{id}")
public ResponseEntity<String> delete(@PathVariable Long id) {
String deletePost = postService.delete(id);
return ResponseEntity.ok().body(deletePost);
}
}
findById() 결과


Dto를 사용함으로써 클라이언트에서 파라미터로 넘어오는 값을 처리하는 코드는 한 줄로 끝냈다.
무엇보다 Dto를 사용함으로써
엔티티 객체는 DB와 매핑 역할만을 수행하게 되고
컨트롤러는 클라이언트 요청을 처리하는 역할만 수행하게 되면서 코드의 계층이 분리됐다.
코드의 계층분리란 각 계층이 독립적으로 관리되는 것을 말하며
이것으로 인해 유지보수 및 테스트 용이성, 확장성과 유연성의 장점을 가지게 됨.