Spring/강의

[📕 기초 Spring] 6-2. 메모장 프로젝트 - ver3 (레이어드 아키텍처 적용)

가지코딩 2025. 5. 5. 16:44

메모장 프로젝트 ver2 의 문제점

  • Controller에 책임이 너무 많다.(요청, 비지니스 로직, 응답, 예외 처리 등)
  • 서버가 종료된 후 다시 켜지면 데이터가 모두 초기화 된다.

📕 목차

  1. Controller 분리하기
  2. 메모 생성 API 리팩토링
  3. 메모 목록 조회 API 리팩토링
  4. 메모 단건 조회 API 리팩토링
  5. 메모 전체 수정 API 리팩토링
  6. 메모 제목 수정 API 리팩토링
  7. 메모 삭제 API 리팩토링
  8. 해결한 문제점 & 문제점

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별로 분리하였다.

 

문제점

  1. 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
  2. 예외 발생시 공통적으로 처리가 불가능하다.
    • 각각의 모든 예외를 try-catch 하여 처리해야 한다.
  3. RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
    • 필요없는 필드에 추가적인 null 검사를 해야한다.
  4. Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
  5. 왜 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