개인프로젝트를 완성했다.
익명 게시판 API를 구현하는 프로젝트 과제 였는데, 스프링 강의와 자바 공부를 병행하며 만들었다.
자바의 정석 예외처리 챕터를 공부하면서 만든 과제라서 예외 처리를 어떻게 해야하는지 공부가 잘 되었다.
아쉬운 점은 KPT에서 적기로 하겠다.
컨트롤러 부분에서 ResponseEntity를 사용헀는데 이 부분은 튜터님에게 도움을 받아서 만든 것이라서 완전히 이해를 못했다. 그래서 공부를 더 하고 나중에 TIL에 쓰고 싶다.
3 Layer Architecture
Controller를 세가지로 나누는 것이다.
@Controller
@Service
@Repository
왜 세가지로 나눴냐?
- Controller에 모든 걸 담아버리면 코드의 길이가 너무 길어지기 때문에 코드를 이해하기 어렵다.
- 코드 수정 사항이 생겼을 때 수정하기가 까다롭다.
- 문제가 발생했는데 해당 Controller를 구현한 개발자가 퇴사를 한 상황이라면?
그래서 개발자들은 Controller가 하는 역할을 세가지로 분리해서 쓰기 시작했다.
@Controller
- 클라이언트의 Request를 받는다.
- 요청에 대한 로직 처리는 Service에게 전담한다.
- Request 데이터가 있다면 Service에게 같이 전달한다.
- Service에서 처리 완료된 결과를 클라이언트에게 응답한다.
@Service
- 사용자의 요구사항을 처리 ('비즈니스 로직') 한다.
- 그래서 현업에서는 서비스 코드가 계속 비대해진다고 한다.
- DB 저장 및 조회가 필요할 때는 Repository에게 요청한다.
@Repository
- DB관리 (연결, 해제, 자원 관리)를 한다.
- DB CRUD 작업을 처리한다.
전체적으로 보면 이런 그림이다
나는 DI (의존성 주입)을 통해 IoC (제어의 역전)을 구현한 3 레이어 아키텍처를 만들었다.
package com.sparta.spartaposts.controller;
import com.sparta.spartaposts.dto.PostRequestDto;
import com.sparta.spartaposts.dto.PostResponseDto;
import com.sparta.spartaposts.service.PostService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
private HttpHeaders makeUTF8Header(){
HttpHeaders returnResHeaders = new HttpHeaders();
returnResHeaders.add("Content-Type", "application/json;charset=UTF-8");
return returnResHeaders;
}
@PostMapping("/posts")
public PostResponseDto creatPost(@RequestBody PostRequestDto postRequestDto) {
return postService.creatPost(postRequestDto);
}
@GetMapping("/posts")
public List<PostResponseDto> getPosts() {
return postService.getPosts();
}
@GetMapping("/posts/{id}")
public ResponseEntity<String> getPost(@PathVariable Long id) {
try{
postService.getPost(id);
}
catch (NullPointerException | IllegalArgumentException e) {
return new ResponseEntity<>("존재하지 않는 게시글입니다.", makeUTF8Header(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(String.valueOf(id), makeUTF8Header(), HttpStatus.OK);
}
@PutMapping("/posts/{id}")
public ResponseEntity<String> updatePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto) {
try {
postService.updatePost(id, postRequestDto);
} catch (IllegalArgumentException e) {
return new ResponseEntity<>(e.getMessage(), makeUTF8Header(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(id + "번이 정상적으로 수정 되었습니다.", makeUTF8Header(), HttpStatus.OK);
}
@DeleteMapping("/posts/{id}")
public ResponseEntity<String> deletePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto) {
try {
postService.deletePost(id, postRequestDto);
} catch (IllegalArgumentException e) {
return new ResponseEntity<>(e.getMessage(), makeUTF8Header(), HttpStatus.BAD_REQUEST);
// 로그 변환해서 e.getMessage를 스트링으로 변환해야함
}
return new ResponseEntity<>("게시글 번호 "+id + "번이 정상적으로 삭제 되었습니다", makeUTF8Header(), HttpStatus.OK);
// 리스폰스 엔티티
}
}
----
package com.sparta.spartaposts.service;
import com.sparta.spartaposts.dto.PostRequestDto;
import com.sparta.spartaposts.dto.PostResponseDto;
import com.sparta.spartaposts.entity.Post;
import com.sparta.spartaposts.repository.PostRepository;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
public PostResponseDto creatPost(PostRequestDto postRequestDto) {
// requestDto -> Entity
Post post = new Post(postRequestDto);
post.setCreatedAt(LocalDateTime.now());
Post savePost = postRepository.save(post);
// Entity -> ResponseDto
PostResponseDto postResponseDto = new PostResponseDto(post);
return postResponseDto;
}
public List<PostResponseDto> getPosts() {
return postRepository.findAll();
}
public PostResponseDto getPost(Long id) {
return postRepository.findPost(id);
}
public Long updatePost(Long id, PostRequestDto postRequestDto) {
return postRepository.editPost(id, postRequestDto);
}
public Long deletePost(Long id, PostRequestDto postRequestDto) {
return postRepository.delete(id, postRequestDto);
}
}
----
package com.sparta.spartaposts.repository;
import com.sparta.spartaposts.dto.PostRequestDto;
import com.sparta.spartaposts.dto.PostResponseDto;
import com.sparta.spartaposts.entity.Post;
import org.springframework.stereotype.Repository;
import java.util.*;
@Repository
public class PostRepository {
private static final Map<Long, Post> postList = new HashMap<>();
public Post save(Post post) {
// Post Max ID Check
Long maxId = !postList.isEmpty() ? Collections.max(postList.keySet()) + 1 : 1;
post.setId(maxId);
// DB 저장
postList.put(post.getId(), post);
return post;
}
public List<PostResponseDto> findAll() {
// Map To List
List<PostResponseDto> responseDtoList = postList.values().stream()
.map(PostResponseDto::new).sorted(Comparator.comparing(PostResponseDto::getCreatedAt).reversed())
.toList();
return responseDtoList;
}
public PostResponseDto findPost(Long id) {
if (postList.containsKey(id)) {
Post post = postList.get(id);
return new PostResponseDto(post);
} else {
throw new IllegalArgumentException("해당 게시글은 존재하지 않습니다.");
}
}
public Long editPost(Long id, PostRequestDto postRequestDto) {
if (Objects.equals(postList.get(id).getPassword(), postRequestDto.getPassword())) {
// 해당 포스트가 DB에 존재?
if(postList.containsKey(id)) {
Post post = postList.get(id);
post.update(postRequestDto);
return id;
} else {
throw new IllegalArgumentException("해당 게시글은 존재하지 않습니다.");
}
} else {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
}
public Long delete(Long id, PostRequestDto postRequestDto) {
if (Objects.equals(postList.get(id).getPassword(), postRequestDto.getPassword())) {
if(postList.containsKey(id)) {
postList.remove(id);
return id;
} else {
throw new IllegalArgumentException("해당 게시글은 존재하지 않습니다.");
}
} else {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); // 3층 분리 후 컨트롤러에서 try catch로 잡아야 함!
}
}
}
이렇게 만들면 DB관련 (내 프로젝트에선 DB 연결은 안했지만) 수정이 필요하면 Repository 부분만 건들면되고
비즈니스 로직 관련 수정이 필요하면 Service 부분만 건들면 된다.
의존성 관련해서는 아직 정확히 이해를 못해서 오늘 TIL에선 다루진 않을 예정이다..
3 계층 분리 아키텍처 (3 레이어 아키텍처) 를 구현하며 느낀 점은 개발자들은 정말 유지보수성을 엄청 중요시 한다는 생각이 들었다.
'TIL' 카테고리의 다른 글
TIL 2023-11-08 영속성 컨텍스트, Jpa의 트랜잭션 (0) | 2023.11.08 |
---|---|
TIL 2023-11-07 Bean과 IoC 컨테이너 (0) | 2023.11.07 |
TIL 2023-11-03 다형성 업 캐스팅 다운 캐스팅 참조 변수 완벽 이해 (0) | 2023.11.03 |
TIL 2023-11-02 Path Variable, Request Param, Model Attribute, Request Body (0) | 2023.11.02 |
TIL 2023-11-01 Spring MVC (0) | 2023.11.01 |