메모장 CRUD

StrayCat·2026년 2월 11일

샘플 코드 전문

// Memo.java
@Getter
@Setter
@NoArgsConstructor
public class Memo {
    private Long id;
    private String username;
    private String contents;

    // 생성자 Constructor
    public Memo(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }

    public void update(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }
}

// MemoRequestDto.java
@Getter
public class MemoRequestDto {
    private String username;
    private String contents;
}

// MemoResponseDto.java
@Getter
public class MemoResponseDto {
    private Long id;
    private String username;
    private String contents;

    public MemoResponseDto(Memo memo) {
        this.id = memo.getId();
        this.username = memo.getUsername();
        this.contents = memo.getContents();
    }
}

// MemoController.java
...
@RestController
@RequestMapping("/api")
public class MemoController {

    private final Map<Long, Memo> memoList = new HashMap<>();

    // 메모 추가
    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        // RequestDto -> Entity로 수정 (DB 저장을 위함)
        Memo memo = new Memo(requestDto);

        // Memo의 Max ID를 찾아야함 (check)
        // keySet은 Map의 key value중 key 값을 참조하고 max는 그중 최대 수를 가져오는 Collections의 메서드
        Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
        memo.setId(maxId);

        // DB 저장
        memoList.put(memo.getId(), memo);

        // Entity -> ResponseDto로 변환 후 반환
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

        return memoResponseDto;
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        // Map To List
        List<MemoResponseDto> responseList = memoList.values().stream()
                .map(MemoResponseDto::new).toList();

        return responseList;
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        // 메모가 실제로 db에 존재하는지 체크
        if (memoList.containsKey(id)) {
            // 해당 메모 가져오기
            Memo memo = memoList.get(id);

            // 가져온 메모를 수정
            memo.update(requestDto);
            return memo.getId();
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        // 메모가 존재하는지 체크
        if (memoList.containsKey(id)) {
            //해당 메모 삭제
            memoList.remove(id);
            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }

}

1. CREATE (createMemo)

public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto)

 $.ajax({
        type: "POST",
        url: "/api/memos",
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function (response) {
          alert('메시지가 성공적으로 작성되었습니다.');
          window.location.reload();
        }
      });
  • Front-End에서 이렇게 호출할 경우 서버는 JSON 자료를 받게 된다.

  • JSON 자료를 미리 양식에 맟춰 만들어둔 MemoRequestDto에 받도록 한다. (@RequestBody 필요)

  • ex)MemoRequestDto 타입의 requestDto 객체에는 API호출로 받아온 JSON 자료의 내용이 삽입됨 {"username":"straycat","contents":"hello"}

  • id값은 서버에서 생성 및 부여하므로 MemoRequestDto의 경우 id필드 존재하지 않음

  • 다음 작업은 requestDto를 Entity(Memo 타입 객체)로 수정해야함

  • 이는 DB 저장을 위함이고, 이를 위해선 id값을 제공해야함

    Q. 그냥 Memo 타입 객체 하나로 다 하면 될 것 같은데, 굳이 반복적인 느낌으로 requestDto, responseDto를 별도로 작성해서 사용하는 이유는?
    -> DTO 분리를 통해 보안, 유연성, 안정성을 위함

  • maxId값 부여 (memoList의 key값과 Memo객체의 id필드는 동일하게 맟춤)

Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
memo.setId(maxId);
  • Collections와 Stream에 대해서는 차후 포스팅 예정
  • 다음은 DB (여기서는 memoList)에 저장
memoList.put(memo.getId(), memo);
  • 위에서 memo의 id필드를 setId(maxId)로 지정했으므로, memoList의 key값은 memo.getId()로 참조해서 동일하게 설정한다.

  • ex) memoList[0] >>> [1, {"id":1,"username":"straycat","contents":"hello"}]

  • 헷갈릴 수 있지만 헷갈리지 말 것.

  • 마지막으로 새로 설정한 Memo객체 Entity를 ResponseDto로 변환 후 리턴

MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
  • responseDto는 Memo객체와 양식이 같으므로 그대로 넣고 객체 생성함
  • 객체로서 return된 memoResponseDto는 Front-End에서 다음과 같이 처리된다. (예시)
function writePost() {
  // ...
  $.ajax({
    type: "POST",
    url: "/api/memos",
    contentType: "application/json",
    data: JSON.stringify(data),
    success: function (response) {  // {id: 5, username: "abc", contents: "...", modifiedAt: "..."}
      alert('메시지가 성공적으로 작성되었습니다.');
      
      // 새로고침 대신 응답 데이터로 바로 추가
      addHTML(response.id, response.username, response.contents, response.modifiedAt);
      $('#contents').val('');  // textarea 초기화
    }
  });
}

Q. Front-End측에서 response에 전달한 responseDto를 사용하지 않을 때도 있지 않나? 굳이 매번 responseDto를 반환해야 하는가? 응답 코드와 메세지로 충분하지 않은가?

A: 실무적인 관례로서 해석

  • REST API의 일반적인 패턴:
  • POST /memos -> 201 Created + 생성된 리소스 반환
  • GET /memos/:id -> 200 OK + 리소스
  • PUT /memos/:id -> 200 OK + 업데이트된 리소스 (or 204 No Content)
  • DELETE /memos/:id -> 204 No Content (or 200 + 삭제 정보)
  • 실무에서 CREATE는 대부분 생성된 데이터를 반환함.
  • Front-End가 즉시 사용할 확률이 높음
  • 반환하지 않으면 결국 해당 데아터에 대한 GET호출을 한번 더 해야함(비효율적)
  • 예외적인 경우 : 파일 업로드 등 용량이 큰 경우의 POST
// 파일 업로드처럼 용량 큰 경우
@PostMapping("/upload")
public UploadResponse upload() {
    return new UploadResponse(fileId, fileUrl);  // 전체 파일은 리턴하지 않음
}

// 배치 처리
@PostMapping("/batch")
public void processBatch() {
    // 비동기 처리, 결과는 나중에 조회
}

2. READ (getMemos)

  • 상황에 따라 다르지만, 대체로 심플
  • Front단에서 요구하는 사양에 맞게 리턴해준다.
  • 예시에서는 Map To List를 통해 Map형식의 memoList를 List로 변환해서 반환함
// Map To List
List<MemoResponseDto> responseList = memoList.values().stream()
                .map(MemoResponseDto::new).toList();
  • stream이 단계적으로 어떻게 일어나는지 알아보자
Map<Long, Memo> memoList = {
  1: Memo{id=1, username="abc", contents="memo1"},
  2: Memo{id=2, username="xyz", contents="memo2"}
};

// 1단계: values() - Map의 값들만 추출 -> key / value중 value만
// List는 key값을 지니지 않으므로 key는 버림
Collection<Memo> values = memoList.values();
// [Memo{id=1, ...}, Memo{id=2, ...}]

// 2단계: stream() - Stream으로 변환 (반복 처리 준비)
Stream<Memo> stream = values.stream();

// 3단계: map(MemoResponseDto::new) - 각 Memo를 ResponseDto로 변환
// MemoResponseDto::new는 memo -> new MemoResponseDto(memo)와 동일
Stream<MemoResponseDto> dtoStream = stream.map(MemoResponseDto::new);
// 결국 1단계에서 만들어둔 Memo객체 Collection의 각 index를 순회하며 new MemoResponseDto(memo)로 생성하는 것

// 4단계: toList() - Stream을 List로 수집
List<MemoResponseDto> responseList = dtoStream.toList();

//map(MemoResponseDto::new) 이것과 map(memo -> new MemoResponseDto(memo))이것이 동일함을 기억

//for문으로도 가능하다
List<MemoResponseDto> responseList = new ArrayList<>();
for (Memo memo : memoList.values()) {
    responseList.add(new MemoResponseDto(memo));
}
  • 클라이언트에 반환한 responseList는 다음과 같이 사용된다 (예시)
$.ajax({
        type: 'GET',
        url: '/api/memos',
        success: function (response) {
          for (let i = 0; i < response.length; i++) {
            let message = response[i];
            let id = message['id'];
            let username = message['username'];
            let contents = message['contents'];
            let modifiedAt = message['modifiedAt'];
            addHTML(id, username, contents, modifiedAt);
          }
        }
      })

Q. 클라이언트는 response 파라미터의 자료형이 List인것을 어떻게 인식하고 처리할 수 있는가?

A. Spring이 자동으로 JSON배열로 변환해서 보내고 있기 때문임
서버 -> 클라이언트 변환 과정을 살펴보자

// 1. 컨트롤러가 List 반환
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
    return responseList;  // Java List
}

// 2. Spring의 Jackson 라이브러리가 자동으로 JSON 변환
// List<MemoResponseDto> → JSON 배열
[
  {"id":1, "username":"abc", "contents":"memo1", "modifiedAt":"2024-01-01"},
  {"id":2, "username":"xyz", "contents":"memo2", "modifiedAt":"2024-01-02"}
]

// 3. 클라이언트는 JSON 배열을 받음
success: function (response) {
    // response는 JavaScript 배열 객체
    console.log(response.length);  // 2
    console.log(response[0].id);   // 1
    console.log(response[0].username); // abc
}
  • 자동변환은 Spring의 @RestController + Jackson이 처리

3. UPDATE (updateMemo)

  • PUT 요청을 사용
  • 예시에서는 @PathVariable 방식으로 id값 전달해서 특정
  • 우선 파라미터로 받은 id값을 이용해 메모가 DB (여기서는 List)에 존재하는지 체크
    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        // 메모가 실제로 db에 존재하는지 체크
        if (memoList.containsKey(id)) {
        ...
  • 메모가 존재해서 가져오는데 성공했다면 (if문 true 통과)
  • Memo객체에 선언해둔 update메서드를 호출해 내용을 수정한다
  • 리턴타입은 API설계에 맞게 설정
 // 가져온 메모를 수정
            memo.update(requestDto);
            return memo.getId();
  • id값이 존재하지 않아 결과 false 로 else로 넘어간다면
} else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
  • 적절히 예외처리를 해준다.

4. DELETE (deleteMemo)

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        // 메모가 존재하는지 체크
        if (memoList.containsKey(id)) {
            //해당 메모 삭제
            memoList.remove(id);
            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
  • UPDATE와 유사한 로직
  • 대신 파라미터로 id값만 받고 다른 필드는 받지 않음
profile
알면 좋은 것보단 잊어버리기 싫은 것들을 기록합니다.

0개의 댓글