메모장 프로젝트 ver2 의 문제점
- Controller에 책임이 너무 많다.(요청, 비지니스 로직, 응답, 예외 처리 등)
- 서버가 종료된 후 다시 켜지면 데이터가 모두 초기화 된다.
📕 목차
- Controller 분리하기
- 메모 생성 API 리팩토링
- 메모 목록 조회 API 리팩토링
- 메모 단건 조회 API 리팩토링
- 메모 전체 수정 API 리팩토링
- 메모 제목 수정 API 리팩토링
- 메모 삭제 API 리팩토링
- 해결한 문제점 & 문제점
1. Controller 분리하기
책임 분리
- Controller
- 클라이언트의 요청을 받는 역할을 수행한다.
- 요청에 대한 처리를 Service Layer에 전달한다.
- Service에서 처리 완료된 결과를 클라이언트에 응답한다.
- Service Layer
- 비지니스 로직 처리
- Repository Layer
- 데이터베이스 상호작용
@RestController
@RequestMapping("/memos")
public class MemoController {
// 데이터베이스(Repository)
private final Map<Long, Memo> memoList = new HashMap<>();
// 1. 요청(Controller)
@PostMapping
public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto requestDto) {
// 2. 비지니스 로직
// MemoId 식별자 계산(Repository)
Long memoId = memoList.isEmpty() ? 1 : Collections.max(memoList.keySet()) + 1;
// 요청받은 데이터로 Memo 객체 생성(Service)
Memo memo = new Memo(memoId, requestDto.getTitle(), requestDto.getContents());
// 3. 데이터베이스 상호작용
// Inmemory DB에 Memo 저장(Repository)
memoList.put(memoId, memo);
// 4. 응답(Controller)
return new ResponseEntity<>(new MemoResponseDto(memo), HttpStatus.CREATED);
}
}
[실습]
책임 분리 준비하기
- 폴더 구조
controller 패키지
- MemoController.java
@RestController
@RequestMapping("/memos")
public class MemoController {
private final MemoService memoService;
// 의존성 주입
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
}
entity 패키지
- Memo.java
@Getter
@AllArgsConstructor
public class Memo {
private Long id;
private String title;
private String contents;
public Memo(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void updateTitle(String title) {
this.title = title;
}
}
service 패키지 생성
- MemoService.java
- MemoServiceImple.java
public interface MemoService {
}
@Service
public class MemoServiceImpl implements MemoService {
private final MemoRepository memoRepository;
// 의존성 주입
public MemoServiceImpl(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
}
service 패키지 생성
- MemoService.java
- MEmoServiceImple.java
public interface MemoRepository {
}
@Repository
public class MemoRepositoryImpl implements MemoRepository {
private final Map<Long, Memo> memoList = new HashMap<>();
}
2. 메모 생성 API 리팩토링
MemoController 클래스의 createMemo() 메서드 - ver2
@PostMapping
public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto dto) {
Long memoId = memoList.isEmpty() ? 1 : Collections.max(memoList.keySet()) + 1;
Memo memo = new Memo(memoId, dto.getTitle(), dto.getContents());
memoList.put(memoId, memo);
return new ResponseEntity<>(new MemoResponseDto(memo), HttpStatus.CREATED);
}
[실습]
Controller
@PostMapping
public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto requestDto) {
return new ResponseEntity<>(memoService.saveMemo(requestDto), HttpStatus.CREATED);
}
Service
public interface MemoService {
MemoResponseDto saveMemo(MemoRequestDto dto);
}
@Override
public MemoResponseDto saveMemo(MemoRequestDto requestDto) {
Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());
Memo savedMemo = memoRepository.saveMemo(memo);
return new MemoResponseDto(savedMemo);
}
Repository
public interface MemoRepository {
Memo saveMemo(Memo memo);
}
@Override
public Memo saveMemo(Memo memo) {
Long memoId = memoList.isEmpty() ? 1 : Collections.max(memoList.keySet()) + 1;
memo.setId(memoId);
memoList.put(memoId, memo);
return memo;
}
3. 메모 목록 조회 API 리팩토링
MemoController 클래스의 findAllMemos() 메서드 - ver2
@GetMapping
public ResponseEntity<List<MemoResponseDto>> findAllMemos() {
List<MemoResponseDto> responseList = new ArrayList<>();
for (Memo memo : memoList.values()) {
MemoResponseDto responseDto = new MemoResponseDto(memo);
responseList.add(responseDto);
}
return new ResponseEntity<>(responseList, HttpStatus.OK);
}
[실습]
Controller
@GetMapping
public ResponseEntity<List<MemoResponseDto>> findAllMemos() {
return new ResponseEntity<>(memoService.findAllMemos(), HttpStatus.OK);
}
Service
public interface MemoService {
...
List<MemoResponseDto> findAllMemos();
}
@Override
public List<MemoResponseDto> findAllMemos() {
return memoRepository.findAllMemos();
}
Repository
public interface MemoRepository {
...
List<MemoResponseDto> findAllMemos();
}
@Override
public List<MemoResponseDto> findAllMemos() {
List<MemoResponseDto> allMemos = new ArrayList<>();
for (Memo memo : memoList.values()) {
MemoResponseDto responseDto = new MemoResponseDto(memo);
allMemos.add(responseDto);
}
return allMemos;
}
4. 메모 단건 조회 API 리팩토링
MemoController 클래스의 findMemoById() 메서드 - ver2
@GetMapping("/{id}")
public ResponseEntity<MemoResponseDto> findMemoById(@PathVariable Long id) {
Memo memo = memoList.get(id);
if (memo == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(new MemoResponseDto(memo), HttpStatus.OK);
}
[실습]
Controller
@GetMapping("/{id}")
public ResponseEntity<MemoResponseDto> findMemoById(@PathVariable Long id) {
return new ResponseEntity<>(memoService.findMemoById(id), HttpStatus.OK);
}
Service
public interface MemoService {
...
MemoResponseDto findMemoById(Long id);
}
@Override
public MemoResponseDto findMemoById(Long id) {
Memo memo = memoRepository.findMemoById(id);
// NPE 방지
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
return new MemoResponseDto(memo);
}
Repository
public interface MemoRepository {
...
Memo findMemoById(Long id);
}
@Override
public Memo findMemoById(Long id) {
return memoList.get(id);
}
5. 메모 전체 수정 API 리팩토링
MemoController 클래스의 updateMemo() 메서드 - ver2
@PutMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateMemo(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
Memo memo = memoList.get(id);
if (memo == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
if (requestDto.getTitle() == null || requestDto.getContents() == null) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
memo.update(requestDto);
return new ResponseEntity<>(new MemoResponseDto(memo), HttpStatus.OK);
}
[실습]
Controller
@PutMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
return new ResponseEntity<>(memoService.updateMemo(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
Service
public interface MemoService {
...
MemoResponseDto updateMemo(Long id, String title, String contents);
}
@Override
public MemoResponseDto updateMemo(Long id, String title, String contents) {
Memo memo = memoRepository.findMemoById(id);
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
if (title == null || contents == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
}
memo.update(title, contents);
return new MemoResponseDto(memo);
}
Repository
public interface MemoRepository {
...
void updateMemo(Long id, Memo memo);
}
@Override
public void updateMemo(Long id, Memo memo) {
memoList.put(id, memo);
}
6. 메모 제목 수정 API 리팩토링
MemoController 클래스의 updateTitle() 메서드 - ver2
@PatchMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateTitle(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
Memo memo = memoList.get(id);
if (memo == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
if (requestDto.getTitle() == null || requestDto.getContents() != null) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
memo.updateTitle(requestDto);
return new ResponseEntity<>(new MemoResponseDto(memo), HttpStatus.OK);
}
[실습]
Controller
@PatchMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateTitle(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
return new ResponseEntity<>(memoService.updateTitle(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
Service
public interface MemoService {
...
MemoResponseDto updateTitle(Long id, String title, String contents);
}
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
Memo memo = memoRepository.findMemoById(id);
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
if (title == null || contents != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title is a required value.");
}
memo.updateTitle(title);
return new MemoResponseDto(memo);
}
Repository
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
Memo memo = memoRepository.findMemoById(id);
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
if (title == null || contents != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title is a required value.");
}
memo.updateTitle(title);
memoRepository.updateMemo(id, memo);
return new MemoResponseDto(memo);
}
7. 메모 삭제 API 리팩토링
MemoController 클래스의 deleteMemo() 메서드 - ver2
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMemo(@PathVariable Long id) {
if (memoList.containsKey(id)) {
memoList.remove(id);
return new ResponseEntity<>(HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
[실습]
Controller
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMemo(@PathVariable Long id) {
memoService.deleteMemo(id);
// 성공한 경우
return new ResponseEntity<>(HttpStatus.OK);
}
Service
public interface MemoService {
...
void deleteMemo(Long id);
}
@Override
public void deleteMemo(Long id) {
Memo memo = memoRepository.findMemoById(id);
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
memoRepository.deleteMemo(id);
}
Repository
public interface MemoRepository {
...
void deleteMemo(Long id);
}
@Override
public void deleteMemo(Long id) {
memoList.remove(id);
}
8. 해결한 문제점 & 문제점
해결한 문제점
- Controller의 책임을 Layer별로 분리하였다.
문제점
- 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
- 예외 발생시 공통적으로 처리가 불가능하다.
- 각각의 모든 예외를 try-catch 하여 처리해야 한다.
- RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
- 필요없는 필드에 추가적인 null 검사를 해야한다.
- Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
- 왜 Interface로 만들어서 구현하여 사용하는지 모른다.
실습 코드
https://github.com/gajicoding/spring-crud-memo/tree/v1.3.0
GitHub - gajicoding/spring-crud-memo
Contribute to gajicoding/spring-crud-memo development by creating an account on GitHub.
github.com
'Spring > 강의' 카테고리의 다른 글
[📕 기초 Spring] 6-4. SQL (Structured Query Language) (0) | 2025.05.06 |
---|---|
[📕 기초 Spring] 6-3. 데이터베이스 (Database) (0) | 2025.05.06 |
[📕 기초 Spring] 6-1. 레이어드 아키텍처(Layered Architecture) (0) | 2025.05.05 |
[📕 기초 Spring] 5-4. 메모장 프로젝트 - ver2 (프론트 컨트롤러, 어댑터 패턴 적용) (0) | 2025.05.04 |
[📕 기초 Spring] 5-3. 메모장 프로젝트 - ver1 (CRUD 실습, MVC 패턴 적용) (0) | 2025.05.04 |