-
[SpringBoot] 3주차 ERD 설계, Docker 연결 및 생성 조회 API 구현Study/SpringBoot 2026. 3. 26. 15:17
이번 주차에는 백엔드 기초인 ERD 설계와 Docker 구성 및 DB 연결, 그리고 게시글 생성/조회 API를 구현했다.
이번 주차 공부 내용
- ERD 설계(Database ERD Design)
- Docker 환경 구성 및 DB 연결(Dockerizing MySQL)
- 게시글 생성 API 구현(Create Post API)
- 게시글 상세 조회 API 구현(Read Post API)
ERD란
: ERD(Entity-Relationship Diagram)란 데이터베이스의 구조를 한눈에 볼 수 있도록 그린 데이터 설계도이다. 서비스에서 다루는 정보(Entity)들이 무엇인지, 그리고 그 정보들이 서로 어떻게 연결(Relationship)되어 있는지를 논리적으로 표현한다.
📌 ERD의 핵심 구성 요소
- 엔티티 (Entity): 관리하고자 하는 정보의 단위. (집합)
예시: 게시글(Post), 회원(User) - 속성 (Attribute): 엔티티가 가지는 세부 정보. (원소)
예시: 제목(title), 내용(content), 작성일 - 관계 (Relationship): 엔티티 간의 논리적 연결. (함수/대응)
예시: 한 명의 회원은 여러 개의 게시글을 작성할 수 있다.
💻 ERD 코드 설계
@Entity // 1. 이 클래스는 DB의 테이블(Entity)라고 선언 public class Post { @Id // 2. ERD의 PK(Primary Key) 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // 3. Auto Increment(1, 2, 3...) 설정 private Long id; @Column(nullable = false) // 4. 이 칸은 비어있으면 안 된다(NOT NULL)는 제약 조건 private String title; @Column(columnDefinition = "TEXT", nullable = false) // 5. 데이터 타입을 'TEXT'로 지정 private String content; }PK(Primary Key)?
테이블에서 각 레코드(행)를 유일하게 식별할 수 있는 하나의 키
특징 :
- Unique (유일성): 중복된 값이 존재할 수 없다.
- Not Null (비어있음 방지): 절대로 비어있을 수 없다.
- Immutable (불변성): 한 번 정해지면 가급적 바꾸지 않는 것이 원칙이다.
인프라 구축 및 DB 연결
🐳 Docker란?
: 애플리케이션과 그게 실행되는 데 필요한 모든 것(라이브러리, 설정, DB 등)을 하나의 박스에 담아주는 기술
📌 핵심 역할
- 환경의 일관성
도커에서 표준 규격 위에서 실행되기 때문에, 어디서든 동일한 결과를 보장한다. - 격리와 보안(깔끔한 로컬 환경 유지)
도커는 컨테이너 안에 프로그램을 가두기 때문에, 내 실제 OS는 깨끗하게 유지된다. - 경량성(실행 고속화)
도커는 컴퓨터의 자원을 효율적으로 나눠 쓰면서 필요한 프로그램만 띄우기에 아주 가볍고 빠르다.
🧬 Docker의 사용 : docker-compose.yml → application.yml 연결
# docker-compose.yml services: db: image: mysql:8.0 # MySQL 8.0 버전을 가져온다 container_name: mysql-db # 컨테이너의 별명 ports: - "3306:3306" # 내 컴퓨터의 3306 포트와 도커의 3306 포트를 연결 (통로 개설) environment: MYSQL_DATABASE: springboot_study # 자동으로 생성할 데이터베이스 이름 MYSQL_ROOT_PASSWORD: 1234 # 접속 비밀번호- 이미지(Image): 붕어빵 틀. MySQL 실행에 필요한 모든 게 들어있다.
- 컨테이너(Container): 틀에서 찍어낸 붕어빵. 실제로 컴퓨터 메모리 위에서 돌아가는 DB 서버이다.
# application.yml spring: datasource: # 1. 주소: localhost의 3306 포트(도커가 열어둔 통로)로 접속 url: jdbc:mysql://localhost:3306/springboot_study?useSSL=false&allowPublicKeyRetrieval=true # 2. 계정 정보: 도커 설정에서 정한 비밀번호와 일치 확인 username: root password: 1234 # 3. 드라이버: MySQL과 대화할 수 있는 언어(Driver)를 사용 driver-class-name: com.mysql.cj.jdbc.Driverdocker-compose를 통해 인프라를 코드(IaC)로 관리하고, 이를 스프링 부트의 application.yml 설정과 매핑하여 환경에 구애받지 않는 안정적인 DB 연결을 구현했다.
기능 구현 Flow(코드 로직)
저번 주차 때 DTO 작성과 Validation 적용을 함으로써 보안과 데이터 정합성을 유치했다.
생성 API는 클라이언트의 데이터를 '검증'하고 '영속화'하는 쓰기(Write) 과정이고, 조회 API는 경로 변수를 통해 데이터를 '식별'하고 '예외 처리'를 수행하는 읽기(Read) 과정이다. 이 과정에서 DTO를 활용해 데이터 노출을 최소화하고, 전역 예외 처리를 통해 시스템의 안정성을 확보했다.
게시글 생성 API (POST)
1. 검증과 데이터 수신
// PostController.java @PostMapping("/api/posts") public ApiResponse<PostResponseDto> createPost(@RequestBody @Valid PostRequestDto requestDto) { // 1. @RequestBody: 클라이언트가 보낸 JSON을 DTO 객체로 변환 // 2. @Valid: DTO에 설정한 @NotBlank 같은 제약 조건을 검증 PostResponseDto responseDto = postService.createPost(requestDto); return ApiResponse.success(responseDto); // 3. ApiResponse로 감싸서 성공 응답 반환 }2. 비즈니스 로직과 영속화
// PostService.java @Transactional // 데이터를 수정, 저장할 때는 이 어노테이션이 꼭 필요하다. public Long createPost(PostRequestDto request) { // 1. DTO를 Entity(Post)로 변환한다. // 외부에서 들어온 DTO를 규격(Entity)에 맞게 재포장한다. // 빌더 패턴(Builder)을 써서 어떤 필드에 어떤 값이 들어가는지 명확하게 했다. Post post = Post.builder() .title(request.getTitle()) .content(request.getContent()) .build(); // 2. DB에 저장하고, 저장된 게시글의 번호(ID)를 반환한다. // postRepository.save(post)가 실행되는 순간 MySQL에 실제 데이터가 쌓인다. // 마지막에 .getId()를 붙여 방금 생성된 행의 고유 번호만 리턴하도록 설계했다. return postRepository.save(post).getId(); }
게시글 조회 API Flow
1. 외부의 HTTP 요청을 자바의 영역으로 가져온다.
// PostController.java @GetMapping("/api/posts/{id}") public ApiResponse<PostResponseDto> getPost(@PathVariable Long id) { // 1. @PathVariable: 주소창의 {id}(예: 5)를 Long id 변수에 대입 // 2. PostService의 getPostDetail 메서드를 호출하여 로직을 위임 PostResponseDto responseDto = postService.getPostDetail(id); // 3. ApiResponse.success: 조회된 데이터를 표준 규격으로 감싸서 내보냄 return ApiResponse.success(responseDto); }2. 데이터를 찾고 없으면 예외를 던지는 판단을 한다.
// PostService.java public PostResponseDto getPostDetail(Long postId) { // 1. postRepository.findById(postId): DB 창고에서 해당 ID를 가진 상자를 찾는다. // 2. .orElseThrow: 만약 상자가 없다면 프로그램이 터지지 않고 에러를 던진다. Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); // 3. PostResponseDto.from(post): 찾은 Entity를 DTO(포장된 상품)로 변환한다. return PostResponseDto.from(post); }3. 실제 MySQL DB와 연결하여 데이터를 가져온다.
// PostRepository.java public interface PostRepository extends JpaRepository<Post, Long> { // JpaRepository를 상속받는 것만으로 findById 같은 조회 기능이 자동 생성된다. // 여기서 영속화된 데이터를 실제 객체(Post)로 복원해 서비스로 전달한다. }4. 서비스에서 던진 에러를 낚아채 JSON으로 변환한다.
// GlobalExceptionHandler.java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(GeneralException.class) public ApiResponse<?> handleGeneralException(GeneralException e) { // 서비스에서 던진 "POST404" 에러를 여기서 잡아서 ApiResponse 실패 포맷으로 반환한다. return ApiResponse.fail(e.getCode(), e.getMessage()); } }
실행 결과
테스트 파일 생성
### 1. 게시글 생성 (POST) POST http://localhost:8080/api/posts Content-Type: application/json { "title": "스프링부트 첫 DB", "content": "10글자 채우기ㅣㅣㅣㅣㅣㅣ" } ### 2. 게시글 조회 (GET) GET http://localhost:8080/api/posts/1게시글 생성 API 실행 결과

게시글 조회 API 실행 결과

❓궁금했던 점
1. 꼭 포트 번호를 3306으로 적어야 할까?
: 가능은 하지만 약속대로 가는게..
- 기본값(Default): MySQL의 기본 통신 포트가 3306으로 약속되어 있다.
- 포트 포워딩 (ports: - "3306:3306"):
- 앞의 3306: 내 컴퓨터(Host)에서 부를 번호
- 뒤의 3306: 도커 컨테이너 안에서 MySQL이 기다리는 번호
- 만약 바꾼다면?: 만약 내 컴퓨터에 이미 다른 MySQL이 깔려 있다면 "3307:3306"으로 설정하고, application.yml의 주소도 localhost:3307로 바꿔야 한다.
2. 영속화(Persistence)란?
수학에서 계산한 결과값을 휘발성 있는 연습장이 아니라, 지워지지 않는 칠판(DB)에 박제하는 것과 같다.
- 개념: 프로그램이 종료되어도 데이터가 사라지지 않고 유지되는 상태
- 코드에서의 영속화: postRepository.save(post)를 호출하는 순간, 메모리(RAM)에만 떠다니던 자바 객체가 MySQL(Disk)이라는 물리적 저장소에 기록되며 영속화된다.
3. 왜 Entity를 직접 노출하지 않고 DTO를 쓰는지?
수학으로 치면 Entity는 모든 중간 과정이 담긴 연습장이고, DTO는 채점자에게 제출하는 답안지다.
- 보안 (Security): Entity에는 비밀번호, 생성일자, 수정일자 등 외부로 나가면 안 되는 민감한 정보가 포함될 수 있다. DTO를 쓰면 딱 필요한 정보만 골라서 보낼 수 있다.
- 독립성 (Decoupling): DB 구조(Entity)가 바뀐다고 해서 클라이언트에게 주는 API 스펙이 매번 바뀌면 안 되므로 DTO가 중간에서 완충 작용을 해준다.
- 순환 참조 방지: 나중에 댓글(Comment) 등이 추가되어 Entity끼리 서로 참조하게 되면, JSON으로 변환할 때 무한 루프에 빠질 수 있는데 DTO는 이를 원천 차단한다.
현재 아키텍처&코드
src/main/java/com/study/springbootstudy/ │ ├── common/ (공통 계층) │ ├── exception/ │ │ ├── GeneralException.java <-- 커스텀 예외 정의 │ │ └── GlobalExceptionHandler.java <-- 전역 예외 처리 로직 (컨트롤러 어드바이스) │ └── ApiResponse.java <-- 표준 API 응답 규격 (성공/실패 공통 포맷) │ ├── controller/ (표현 계층) │ ├── PingController.java <-- 서버 상태 체크용 │ └── PostController.java <-- 게시글 관련 API 요청 수신 │ ├── domain/ (도메인/데이터 모델) │ └── Post.java <-- 핵심 비즈니스 객체 및 DB 엔티티 │ ├── dto/ (데이터 전송 객체) │ ├── PostRequestDto.java <-- 클라이언트로부터 받는 데이터 (생성용) │ └── PostResponseDto.java <-- 클라이언트에게 주는 데이터 (조회용) │ ├── repository/ (데이터 접근 계층) │ └── PostRepository.java <-- DB와의 통신을 담당하는 인터페이스 │ ├── service/ (비즈니스 로직 계층) │ └── PostService.java <-- 핵심 서비스 로직 구현체 │ └── SpringbootStudyApplication.java <-- 프로젝트의 시작점 (메인 클래스)// PostController.java package com.study.springbootstudy.controller; import com.study.springbootstudy.common.ApiResponse; import com.study.springbootstudy.dto.PostRequestDto; import com.study.springbootstudy.dto.PostResponseDto; import com.study.springbootstudy.service.PostService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor // Service를 주입받기 위해 꼭 필요 @RequestMapping("/api/posts") // 이 컨트롤러의 기본 주소를 설정 public class PostController { private final PostService postService; // 1. 게시글 생성 API @PostMapping public ApiResponse<Long> createPost(@Valid @RequestBody PostRequestDto request) { Long savedPostId = postService.createPost(request); return ApiResponse.onSuccess(savedPostId); // 성공 시 저장된 게시글의 ID를 반환 } // 2. 게시글 상세 조회 API @GetMapping("/{postId}") public ApiResponse<PostResponseDto> getPost(@PathVariable Long postId) { PostResponseDto response = postService.getPostDetail(postId); return ApiResponse.onSuccess(response); // 성공 시 게시글 상세 정보를 반환 } }// Post.java package com.study.springbootstudy.domain; import jakarta.persistence.*; import lombok.*; @Entity // 이 클래스가 DB의 테이블 역할을 한다는 선언 @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Post { @Id // 기본키(PK)로 지정 @GeneratedValue(strategy = GenerationType.IDENTITY) // ID를 1, 2, 3... 자동으로 늘려줌 private Long id; @Column(nullable = false) // 빈 값을 허용하지 않음 private String title; @Column(columnDefinition = "TEXT", nullable = false) private String content; }// PostResponsedto.java package com.study.springbootstudy.dto; import com.study.springbootstudy.domain.Post; import lombok.Builder; import lombok.Getter; @Getter @Builder public class PostResponseDto { private Long id; private String title; private String content; // DB에서 꺼낸 Post(Entity)를 DTO로 변환해 주는 메서드 public static PostResponseDto from(Post post) { return PostResponseDto.builder() .id(post.getId()) .title(post.getTitle()) .content(post.getContent()) .build(); } }// PostRepository.java package com.study.springbootstudy.repository; import com.study.springbootstudy.domain.Post; import org.springframework.data.jpa.repository.JpaRepository; // JpaRepository를 상속받으면 기본적인 CRUD(생성, 조회, 수정, 삭제) 기능이 생긴다 public interface PostRepository extends JpaRepository<Post, Long> { }// PostService.java package com.study.springbootstudy.service; import com.study.springbootstudy.common.exception.GeneralException; import com.study.springbootstudy.domain.Post; import com.study.springbootstudy.dto.PostRequestDto; import com.study.springbootstudy.dto.PostResponseDto; import com.study.springbootstudy.repository.PostRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @Transactional(readOnly = true) // 기본적으로 읽기 전용으로 설정하여 성능을 높인다. public class PostService { private final PostRepository postRepository; @Transactional // 데이터를 수정, 저장할 때는 이 어노테이션이 꼭 필요하다. public Long createPost(PostRequestDto request) { // 1. DTO를 Entity(Post)로 변환 Post post = Post.builder() .title(request.getTitle()) .content(request.getContent()) .build(); // 2. DB에 저장하고, 저장된 게시글의 번호(ID)를 반환 return postRepository.save(post).getId(); } public PostResponseDto getPostDetail(Long postId) { // 1. DB에서 ID로 게시글을 찾고, 없으면 예외를 띄움 Post post = postRepository.findById(postId) .orElseThrow(() -> new GeneralException("POST404", "해당 게시글이 존재하지 않습니다.")); // 2. 찾은 게시글을 DTO에 담아 컨트롤러로 보냄 return PostResponseDto.from(post); } }'Study > SpringBoot' 카테고리의 다른 글
[SpringBoot] 6주차 페이지네이션(Pagination), N+1 문제 해결 (0) 2026.05.07 [SpringBoot] 5주차 JWT 인증/인가(회원가입 및 로그인) API 구현 (0) 2026.04.30 [SpringBoot] 4주차 게시판(수정, 삭제), 댓글(생성, 삭제) API 구현 및 Swagger 명세화 (1) 2026.04.02 [SpringBoot] 2주차 예외 처리 및 Vaildation 적용 (0) 2026.03.19 [SpringBoot] 1주차 JAVA 이해 및 ping API 구현 (0) 2026.03.10