지금까지 Spring Boot 입문 흐름을 따라오면서 아래 내용을 정리했습니다.

  • 프로젝트 생성
  • Controller 만들기
  • URL과 요청 이해하기
  • @RequestParam으로 값 받기
  • Controller, Service, Repository 나누기
  • DB 연결하기
  • JPA 처음 보기

이번 글에서는 이 내용을 묶어서 아주 간단한 CRUD API를 만들어보겠습니다.

CRUD는 백엔드 개발에서 가장 기본이 되는 흐름입니다.

Create = 생성
Read = 조회
Update = 수정
Delete = 삭제

회원 데이터를 기준으로 등록, 조회, 수정, 삭제 API를 만들어보겠습니다.

이번 글의 목표는 딱 하나입니다.

백엔드 포트폴리오의 기본 뼈대 완성하기

이번 글은 시리즈 마지막 단계라서 앞선 글보다 조금 길게 느껴질 수 있습니다.

처음에는 모든 코드를 한 번에 따라 치기보다, 먼저 아래 CRUD 흐름과 요청 구조를 전체적으로 보는 것만으로도 충분합니다.


CRUD란?

CRUD는 데이터를 다루는 가장 기본적인 4가지 기능입니다.

이름의미예시
Create데이터 생성회원 등록
Read데이터 조회회원 목록 보기
Update데이터 수정회원 이름 변경
Delete데이터 삭제회원 삭제

대부분의 백엔드 기능은 이 CRUD에서 시작합니다.

예를 들어 게시판을 만든다면:

  • 글 작성 = Create
  • 글 조회 = Read
  • 글 수정 = Update
  • 글 삭제 = Delete

상품 관리도 마찬가지입니다.

  • 상품 등록
  • 상품 조회
  • 상품 수정
  • 상품 삭제

즉, CRUD를 이해하면 백엔드 API의 기본 흐름을 잡을 수 있습니다.


오늘 만들 API

이번 글에서는 회원 데이터를 기준으로 아래 API를 만들어보겠습니다.

기능HTTP 메서드주소
회원 등록POST/members
회원 전체 조회GET/members
회원 단건 조회GET/members/{id}
회원 이름 수정PUT/members/{id}
회원 삭제DELETE/members/{id}

처음 보면 주소가 조금 많아 보일 수 있습니다.

하지만 기준은 단순합니다.

/members       → 회원 목록 또는 회원 생성
/members/{id}  → 특정 회원 하나

{id}는 회원 번호가 들어가는 자리입니다.

예를 들어 1번 회원을 조회하려면 아래 주소를 사용합니다.

/members/1

전체 구조

이번 예제는 아래 구조로 나눠서 작성합니다.

Member
MemberRepository
MemberService
MemberController
MemberRequest

각 역할은 다음과 같습니다.

파일역할
Member회원 Entity
MemberRepositoryDB 저장/조회 담당
MemberService회원 관련 로직 처리
MemberControllerAPI 요청 받기
MemberRequest요청으로 들어온 값 받기

처음에는 파일이 많아 보여도 역할로 보면 단순합니다.

Controller = 요청 받기
Service = 규칙 처리하기
Repository = DB 다루기
Entity = DB에 저장될 객체
Request = 요청 데이터 받기

1. Member Entity 만들기

먼저 회원 정보를 DB에 저장할 Entity를 만듭니다.

Member.java

package com.example.demo;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    protected Member() {
    }

    public Member(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void changeName(String name) {
        this.name = name;
    }
}

Entity 코드 설명

@Entity

@Entity
public class Member {
}

@Entity는 이 클래스가 DB 테이블과 연결될 객체라는 뜻입니다.

쉽게 말하면 Member 객체를 DB에 저장할 수 있게 표시하는 것입니다.


@Id

@Id
private Long id;

@Id는 기본 키를 의미합니다.

회원이 여러 명 있을 때 각각을 구분할 번호가 필요합니다.

그 역할을 하는 값이 id입니다.


@GeneratedValue

@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@GeneratedValue는 id 값을 자동으로 만들어달라는 뜻입니다.

회원을 저장할 때 우리가 직접 id를 넣지 않아도 DB가 번호를 만들어줍니다.


changeName()

public void changeName(String name) {
    this.name = name;
}

회원 이름을 수정할 때 사용할 메서드입니다.

필드를 직접 바꾸는 것보다, 이렇게 의미 있는 메서드로 바꾸는 습관을 들이면 좋습니다.


2. Repository 만들기

이제 DB에 접근할 Repository를 만듭니다.

MemberRepository.java

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

코드는 짧지만 중요한 역할을 합니다.

JpaRepository를 상속하면 기본적인 저장, 조회, 삭제 기능을 사용할 수 있습니다.

예를 들어:

메서드역할
save()저장 또는 수정
findAll()전체 조회
findById()id로 조회
delete()삭제

우리가 직접 SQL을 많이 작성하지 않아도 기본 CRUD를 만들 수 있습니다.


3. 요청 값을 받을 DTO 만들기

회원 등록이나 수정 요청에서는 이름 값을 받아야 합니다.

그 값을 받을 클래스를 하나 만듭니다.

MemberRequest.java

package com.example.demo;

public class MemberRequest {

    private String name;

    public MemberRequest() {
    }

    public String getName() {
        return name;
    }
}

DTO는 무엇일까?

DTO는 데이터를 주고받기 위한 객체입니다.

여기서는 클라이언트가 보낸 JSON 값을 Java 객체로 받기 위해 사용합니다.

예를 들어 Postman에서 아래 JSON을 보낸다고 해보겠습니다.

{
  "name": "kim"
}

Spring Boot는 이 값을 MemberRequest 객체에 넣어줍니다.

즉, name 값으로 kim이 들어오게 됩니다.


4. Service 만들기

이제 실제 CRUD 로직을 처리할 Service를 만듭니다.

MemberService.java

package com.example.demo;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Transactional
    public Member create(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
        }

        Member member = new Member(name);
        return memberRepository.save(member);
    }

    public List<Member> findAll() {
        return memberRepository.findAll();
    }

    public Member findOne(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다."));
    }

    @Transactional
    public Member update(Long id, String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
        }

        Member member = findOne(id);
        member.changeName(name);
        return member;
    }

    @Transactional
    public void delete(Long id) {
        Member member = findOne(id);
        memberRepository.delete(member);
    }
}

Service 코드 설명

회원 생성

@Transactional
public Member create(String name) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
    }

    Member member = new Member(name);
    return memberRepository.save(member);
}

회원 이름을 검사한 뒤, 문제가 없으면 Member 객체를 만들고 저장합니다.

save()JpaRepository가 제공하는 메서드입니다.


전체 조회

public List<Member> findAll() {
    return memberRepository.findAll();
}

DB에 저장된 모든 회원을 조회합니다.


단건 조회

public Member findOne(Long id) {
    return memberRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다."));
}

id로 회원 한 명을 찾습니다.

해당 id의 회원이 없으면 예외를 발생시킵니다.


수정

@Transactional
public Member update(Long id, String name) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
    }

    Member member = findOne(id);
    member.changeName(name);
    return member;
}

먼저 id로 회원을 찾고, 이름을 바꿉니다.

JPA에서는 트랜잭션 안에서 Entity 값을 변경하면 변경 내용이 DB에 반영될 수 있습니다.

초보 단계에서는 이렇게 이해하면 됩니다.

DB에서 회원을 찾고,
객체의 이름을 바꾸면,
JPA가 변경 내용을 DB에 반영한다.

삭제

@Transactional
public void delete(Long id) {
    Member member = findOne(id);
    memberRepository.delete(member);
}

id로 회원을 찾은 뒤 삭제합니다.


@Transactional은 무엇일까?

@Transactional은 DB 작업을 하나의 단위로 묶어주는 역할을 합니다.

처음에는 어렵게 생각하지 않아도 됩니다.

저장, 수정, 삭제처럼 DB 내용이 바뀌는 작업에는 붙이는 경우가 많습니다.

쉽게 말하면:

이 메서드 안의 DB 작업을 안전하게 처리해 주세요.

정도로 이해하면 됩니다.


5. Controller 만들기

이제 외부 요청을 받을 Controller를 만듭니다.

MemberController.java

package com.example.demo;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping
    public Member create(@RequestBody MemberRequest request) {
        return memberService.create(request.getName());
    }

    @GetMapping
    public List<Member> findAll() {
        return memberService.findAll();
    }

    @GetMapping("/{id}")
    public Member findOne(@PathVariable Long id) {
        return memberService.findOne(id);
    }

    @PutMapping("/{id}")
    public Member update(
            @PathVariable Long id,
            @RequestBody MemberRequest request
    ) {
        return memberService.update(id, request.getName());
    }

    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id) {
        memberService.delete(id);
        return "deleted";
    }
}

Controller 코드 설명

@RequestMapping("/members")

@RequestMapping("/members")
public class MemberController {
}

이 컨트롤러의 기본 주소를 /members로 정합니다.

그래서 아래 메서드들은 모두 /members를 기준으로 동작합니다.


Create: 회원 등록

@PostMapping
public Member create(@RequestBody MemberRequest request) {
    return memberService.create(request.getName());
}

POST /members 요청을 처리합니다.

@RequestBody는 요청 본문에 담긴 JSON 값을 Java 객체로 바꿔줍니다.

예를 들어 아래 JSON을 보내면:

{
  "name": "kim"
}

MemberRequestnamekim이 들어갑니다.


Read: 전체 조회

@GetMapping
public List<Member> findAll() {
    return memberService.findAll();
}

GET /members 요청을 처리합니다.

저장된 회원 목록 전체를 반환합니다.


Read: 단건 조회

@GetMapping("/{id}")
public Member findOne(@PathVariable Long id) {
    return memberService.findOne(id);
}

GET /members/1 같은 요청을 처리합니다.

여기서 1은 id입니다.

@PathVariable은 URL 경로에 들어 있는 값을 Java 변수로 받아줍니다.


Update: 회원 수정

@PutMapping("/{id}")
public Member update(
        @PathVariable Long id,
        @RequestBody MemberRequest request
) {
    return memberService.update(id, request.getName());
}

PUT /members/1 요청을 처리합니다.

1번 회원의 이름을 요청 본문에 들어온 이름으로 수정합니다.


Delete: 회원 삭제

@DeleteMapping("/{id}")
public String delete(@PathVariable Long id) {
    memberService.delete(id);
    return "deleted";
}

DELETE /members/1 요청을 처리합니다.

1번 회원을 삭제합니다.


HTTP 메서드와 CRUD 연결

이번 예제에서는 HTTP 메서드를 이렇게 사용했습니다.

CRUDHTTP 메서드주소설명
CreatePOST/members회원 생성
ReadGET/members전체 조회
ReadGET/members/{id}단건 조회
UpdatePUT/members/{id}회원 수정
DeleteDELETE/members/{id}회원 삭제

처음에는 이렇게 기억하면 됩니다.

POST = 만들기
GET = 조회하기
PUT = 수정하기
DELETE = 삭제하기

Postman으로 테스트하기

브라우저 주소창만으로는 POST, PUT, DELETE 요청을 보내기 어렵습니다.

그래서 보통 Postman 같은 도구를 사용합니다.

Postman은 API 요청을 쉽게 보내볼 수 있는 도구입니다.


1. 회원 등록 테스트

요청 정보는 아래처럼 설정합니다.

Method: POST
URL: http://localhost:8080/members
Body: raw / JSON

Body에는 아래 JSON을 입력합니다.

{
  "name": "kim"
}

응답 예시는 다음과 같습니다.

{
  "id": 1,
  "name": "kim"
}

2. 회원 전체 조회 테스트

Method: GET
URL: http://localhost:8080/members

응답 예시는 다음과 같습니다.

[
  {
    "id": 1,
    "name": "kim"
  }
]

3. 회원 단건 조회 테스트

Method: GET
URL: http://localhost:8080/members/1

응답 예시는 다음과 같습니다.

{
  "id": 1,
  "name": "kim"
}

4. 회원 수정 테스트

Method: PUT
URL: http://localhost:8080/members/1
Body: raw / JSON

Body에는 아래 JSON을 입력합니다.

{
  "name": "park"
}

응답 예시는 다음과 같습니다.

{
  "id": 1,
  "name": "park"
}

5. 회원 삭제 테스트

Method: DELETE
URL: http://localhost:8080/members/1

응답은 아래처럼 나올 수 있습니다.

deleted

삭제 후 다시 전체 조회를 하면 목록에서 해당 회원이 사라져야 합니다.


브라우저로 테스트할 수 있는 것

브라우저 주소창으로는 주로 GET 요청을 테스트하기 좋습니다.

예를 들어:

http://localhost:8080/members
http://localhost:8080/members/1

하지만 회원 등록, 수정, 삭제는 브라우저 주소창만으로 테스트하기 불편합니다.

그래서 CRUD API를 만들 때는 Postman을 함께 사용하는 것이 좋습니다.


자주 헷갈리는 부분

1. @RequestParam@RequestBody는 뭐가 다를까?

@RequestParam은 주소 뒤에 붙은 값을 받을 때 사용합니다.

/members/new?name=kim

반면 @RequestBody는 요청 본문에 담긴 JSON을 받을 때 사용합니다.

{
  "name": "kim"
}

즉, 차이는 이렇게 볼 수 있습니다.

@RequestParam = 주소 뒤의 값
@RequestBody = 요청 본문의 JSON

2. @PathVariable은 언제 쓸까?

@PathVariable은 URL 경로 안에 있는 값을 받을 때 사용합니다.

예를 들어:

/members/1

여기서 1을 Java 변수로 받고 싶다면 @PathVariable을 사용합니다.

@GetMapping("/{id}")
public Member findOne(@PathVariable Long id) {
    return memberService.findOne(id);
}

3. 왜 저장은 POST를 쓸까?

GET은 보통 데이터를 조회할 때 사용합니다.

데이터를 새로 만들 때는 POST를 사용합니다.

그래서 회원 등록 API는 이렇게 만듭니다.

POST /members

4. 왜 수정은 PUT을 쓸까?

PUT은 기존 데이터를 수정할 때 자주 사용합니다.

그래서 회원 이름 수정은 이렇게 만듭니다.

PUT /members/1

1번 회원을 수정한다는 의미입니다.


5. Entity를 바로 응답으로 반환해도 괜찮을까?

학습용 예제에서는 괜찮습니다.

하지만 실제 프로젝트에서는 Entity를 그대로 응답하기보다, 응답 DTO를 따로 만들어 반환하는 경우가 많습니다.

이번 글에서는 CRUD 흐름을 단순하게 이해하기 위해 Entity를 바로 반환했습니다.


오늘 배운 것

이번 글에서는 간단한 회원 CRUD API를 만들어봤습니다.

핵심은 아래와 같습니다.

  • CRUD는 생성, 조회, 수정, 삭제를 의미함
  • POST는 생성에 사용
  • GET은 조회에 사용
  • PUT은 수정에 사용
  • DELETE는 삭제에 사용
  • @RequestBody는 JSON 요청을 Java 객체로 받음
  • @PathVariable은 URL 경로 값을 Java 변수로 받음
  • Postman을 사용하면 API를 쉽게 테스트할 수 있음

정리

이번 글에서는 Spring Boot로 가장 기본적인 CRUD API를 만들어봤습니다.

흐름은 아래와 같습니다.

요청 받기
→ Service에서 로직 처리
→ Repository로 DB 접근
→ 결과 응답

이 구조는 회원 기능뿐만 아니라 게시글, 댓글, 상품, 주문 기능에도 반복해서 사용됩니다.

즉, CRUD API를 한 번 이해하면 백엔드 포트폴리오의 기본 뼈대를 만들 수 있습니다.

처음부터 완벽한 프로젝트를 만들 필요는 없습니다.

먼저 작은 기능을 기준으로 아래 흐름을 직접 만들어보는 것이 중요합니다.

Create
Read
Update
Delete

이제 이 구조 위에 검증, 예외 처리, DTO 분리, 로그인, 배포 같은 기능을 하나씩 붙이면 더 실제 서비스에 가까워집니다.