ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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)?

    테이블에서 각 레코드(행)를 유일하게 식별할 수 있는 하나의 키
    특징 :
    1. Unique (유일성): 중복된 값이 존재할 수 없다.
    2. Not Null (비어있음 방지): 절대로 비어있을 수 없다.
    3. Immutable (불변성): 한 번 정해지면 가급적 바꾸지 않는 것이 원칙이다.

     


    인프라 구축 및 DB 연결

    🐳 Docker란?

     : 애플리케이션과 그게 실행되는 데 필요한 모든 것(라이브러리, 설정, DB 등)을 하나의 박스에 담아주는 기술

     

     

    📌 핵심 역할

    1. 환경의 일관성
      도커에서 표준 규격 위에서 실행되기 때문에, 어디서든 동일한 결과를 보장한다.
    2. 격리와 보안(깔끔한 로컬 환경 유지)
      도커는 컨테이너 안에 프로그램을 가두기 때문에, 내 실제 OS는 깨끗하게 유지된다.
    3. 경량성(실행 고속화)
      도커는 컴퓨터의 자원을 효율적으로 나눠 쓰면서 필요한 프로그램만 띄우기에 아주 가볍고 빠르다.

     

    🧬 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.Driver

    docker-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);
        }
    }

제목 없는 코딩 블로그