[Java] 게시판 만들기 - 1

2025. 9. 7. 19:41·개발/java
반응형

프로젝트 설계 부분은 이전 게시글을 참고하자.

 

[Java] 게시판 만들기 - 0

백엔드 공부용으로 CRUD 기능을 갖는 아주 간단한 게시판을 만들어보고자 한다. 본격적인 개발에 앞서 기본적인 서비스의 요구사항부터 정의해보자. 1. 요구사항로그인은 생략(나중에 추가) 메인

daniellee09.tistory.com

 

설계해둔 Class Diagram을 참고하여 Bottom-up 방식으로 개발을 진행하고자 한다. 그 이유는 다음과 같다.

  1. 의존성 방향: 상위 계층이 하위 계층에 의존하므로 하위부터 구현
  2. 테스트 용이성: 각 계층 완성 후 즉시 테스트 가능
  3. 안정성: 기반이 되는 부분부터 견고하게 구축

가장 먼저 프로젝트의 초기 설정부터 진행해보자.

 

1. 초기 설정

우선 Spring Initializer를 통해 프로젝트를 생성한다.  

https://start.spring.io/

 

아래처럼 메타데이터와 의존성을 설정했다. 

  Project: Gradle Project
  Language: Java
  Spring Boot: 3.5.4
  Project Metadata:
    - Group: board
    - Artifact: board-spring
    - Name: board-spring
    - Description: (기본값)
    - Package name: board.board_spring
    - Packaging: Jar
    - Java: 17
    
    Dependencies:
    ✓ Spring Web - 스프링 MVC, REST API
    ✓ Spring Data JPA - JPA, Hybernate
    ✓ H2 Database - 인메모리 데이터베이스 
    ✓ Thymeleaf - 템플릿 엔진
    ✓ Validation - Bean Validation
    ✓ Lombok - 보일러플레이트 코드 제거
    ✓ jackson-databind:2.17.0 - JSON 처리

 

application.yml 설정

Spring Boot의 중앙 설정 파일로, 이 프로젝트의 모든 환경 설정을 관리하는 application.yml을 설정해주자.

src/main/resources/application.yml

# DB 설정
spring:
  # h2 콘솔 설정 
  h2:
    console:
      enabled: true # h2 웹 콘솔 활성화 
      path: /h2
  datasource:
    url: jdbc:h2:mem:test # h2 인메모리 DB
    driver-class-name: org.h2.Driver
    username: **
    password: ****
  jpa:
    hibernate:
      ddl-auto: update # 테이블 자동 생성/수정
    show-sql: true # SQL 쿼리 출력
    properties:
      hibernate:
        format_sql: true # SQL 쿼리 포맷팅 
  thymeleaf:
    cache: false
server:
  port: 8080 #서버 포트

환경별로 yml 파일을 작성하기도 하지만, 작은 프로젝트 이므로 하나로 설정을 마치겠다.

이제 본격적으로 각 레이어들의 개발을 시작해보자.

 

2. Domain Layer (Entity)

다음과 같은 순서로 게시판의 Board Entity를 만들어 보겠다.

@Entity → Board 엔티티 생성
- 필드 정의 (id, title, content, timestamps)
- 검증 로직 추가
- Static Factory Method 구현

Board.java

package board.board_spring.entity;

import board.board_spring.dto.BoardPatchDto;
import board.board_spring.dto.BoardPostDto;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA용, 외부 직접 생성 방지
public class Board {

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

    private String title;

    private String content;

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // private 생성자
    private Board(String title, String content) {
        this.title = title;
        this.content = content;
    }

    // Create
    // Entity가 Dto를 받아서 생성 , Dto는 데이터 전송에만 집중해야함
    public static Board postFrom(BoardPostDto postDto) {
        return Board.createBoard(postDto.getTitle(), postDto.getContent());
    }

    // 정적 팩토리 메서드 - 생성 의도가 명확해서 좋은 듯
    // 용도? Entity 자체의 생성과 도메인 규칙 관리
    public static Board createBoard(String title, String content) {
        return new Board(title, content);
    }

    // Update
    // 부분 수정
    public void updateFrom(BoardPatchDto patchDto) {

        boolean isUpdated = false; // 업데이트 여부

        if(patchDto.hasTitle()) {
            validateTitle(patchDto.getTitle());
            this.title = patchDto.getTitle();
            isUpdated = true;
        }

        if(patchDto.hasContent()) {
            validateContent(patchDto.getContent());
            this.content = patchDto.getContent();
            isUpdated = true;
        }


    }

    // 전체 수정
    public void updateBoard(String title, String content) {
        validateTitle(title);
        validateContent(content);
        this.title = title;
        this.content = content;
    }

    // 업데이트 검증 로직
    // 제목 검증
    private static void validateTitle(String title) {
        if(title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("제목 입력은 필수입니다."); // 잘못된 인수인지 판단
        }
        if(title.length() > 100) {
            throw new IllegalArgumentException("제목은 100자를 초과할 수 없습니다.");
        }
    }

    // 내용 검증
    private static void validateContent(String content) {
        if(content == null || content.trim().isEmpty()) {
            throw new IllegalArgumentException("내용 입력은 필수입니다.");
        }
        if(content.length() > 8000) {
            throw new IllegalArgumentException("내용은 8000자를 초과할 수 없습니다.");
        }
    }


}

 

각 주요 부분 별로 어떻게 작성되었는지 살펴보자.

클래스 선언부

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {

@Entity

  • JPA가 관리하는 엔티티 클래스임을 선언
  • 데이터베이스 테이블과 1:1 매핑 

@Getter(Lombock)

  • 모든 필드에 대한 getter 메서드 자동 생성
  • getTitle(), getContent() 등을 자동으로 만들어줌

@NoArgsConstructor(access = AccessLevel.PROTECTED)

  • 파라미터 없는 기본 생성자 생성
  • PROTECTED로 외부에서 직접 생성 방지
  • JPA는 프록시 생성 시 기본 생성자 필요

 

필드 설정

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

    private String title;

    private String content;

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

 

@ID

- primary key 필드임을 지정

- 엔티티의 고유 식별자 

- boardID가 바로 각 게시글의 고유 식별자 

 

@GeneratedValue(strategy = GenerationType.IDENTITY)

- PK 값 자동 생성 전략

- IDENTITY: DB의 AUTO_INCREMENT 사용

- MySQL, H2 등에서 자동 증가값 사용 

 

- title 과 content를 통해 게시글의 제목과 내용 저장 

 

@CreationTimestamp

- 엔티티 생성 시 자동으로 현재 시간 설정

- Hibernate 전용 어노테이션

 

@UpdateTimestamp

- 엔티티 수정 시마다 자동으로 현재 시간 갱신

 

@Column 속성

- name: 실제 DB 컬럼명 지정

- nullable = false: NOT NULL 제약조건

- updatable = false: 한번 생성되면 수정 불가 

 

생성자 패턴

// private 생성자
private Board(String title, String content) {
    this.title = title;
    this.content = content;
}

Private 생성자

- 외부에서 new Board() 직접 호출 방지

- 객체 생성을 정적 팩토리 메서드로 제한

- 생성 로직을 캡슐화 

 

정적 팩토리 메서드

// DTO를 받아서 엔티티 생성
public static Board postFrom(BoardPostDto postDto) {
    return Board.createBoard(postDto.getTitle(), postDto.getContent());
}

// 핵심 생성 메서드
public static Board createBoard(String title, String content) {
    return new Board(title, content);
}

정적 팩토리 메서드 패턴을 사용한 이유는 다음과 같다.

  1. 이름을 따로 가질 수 있다: createBoard와 같이 생성 의도가 명확해진다.
  2. 생성 전 검증 가능: 유효성 검사 후 생성
  3. 캐싱 가능: 필요시 인스턴스 재사용
  4. 하위 타입 반환 가능: 유연한 설계

postFrom()과 createBoard의 기능을 비교하면:

- postFrom(): DTO -> Entity 변환용 (Controller/Service에서 사용)

- createBoard(): 순수 도메인 로직용 (테스트, 내부 로직에서 사용)

 

업데이트 메서드

// 부분 수정 (PATCH)
public void updateFrom(BoardPatchDto patchDto) {
    boolean isUpdated = false;
    
    if(patchDto.hasTitle()) {
        validateTitle(patchDto.getTitle());
        this.title = patchDto.getTitle();
        isUpdated = true;
    }
    
    if(patchDto.hasContent()) {
        validateContent(patchDto.getContent());
        this.content = patchDto.getContent();
        isUpdated = true;
    }
}

// 전체 수정 (PUT)
public void updateBoard(String title, String content) {
    validateTitle(title);
    validateContent(content);
    this.title = title;
    this.content = content;
}

 

부분 수정과 전체 수정 두 가지로 메소드를 분리해서 구현했다.

- updateFrom(): PATCH 요청용, null이 아닌 필드만 수정

- updateBoard(): PUT 요청용, 모든 필드 수정

 

hasTitle(), hasContent():

- DTO에서 해당 필드가 실제로 전달되었는지 확인한다.

- null과 "의도적으로 비운 값"을 구분하는 역할

 

검증 로직

private static void validateTitle(String title) {
    if(title == null || title.trim().isEmpty()) {
        throw new IllegalArgumentException("제목 입력은 필수입니다.");
    }
    if(title.length() > 100) {
        throw new IllegalArgumentException("제목은 100자를 초과할 수 없습니다.");
    }
}

private static void validateContent(String content) {
    if(content == null || content.trim().isEmpty()) {
        throw new IllegalArgumentException("내용 입력은 필수입니다.");
    }
    if(content.length() > 8000) {
        throw new IllegalArgumentException("내용은 8000자를 초과할 수 없습니다.");
    }
}

특징

- private static: 인스턴스 생성 전에도 호출 가능

- IllegalArgumentException: 잘못된 인자 전달 시 예외 처리

- trim(): 공백만 있는 문자열도 빈 값으로 처리 

- 도메인 규칙 적용: 제목 100자, 내용 8000자 제한 

 

3. 설계 원칙과 패턴

Domain을 설계하면서 다음과 같은 원칙들을 최대한 고려했다.

  1. 캡슐화
    • setter 없음 -> 무분별한 수정 방지
    • 업데이트는 의미있는 메서드로만 가능
  2. 불변성 추구
    • 생성 후 ID, 생성일시는 변경 불가
    • 명시적인 업데이트 메서드로만 수정 가능
  3. 도메인 주도 설계(DDD)
    • 엔티티가 자신의 규칙을 관리
    • 비즈니스 로직이 도메인 객체에 모여있음
  4. 계층 분리
    • DTO와 Entity 명확히 분리
    • 각 계층의 책임을 명확히 함

이제 도메인이 완성되었으니 다음으로 Repository를 구현해보겠다. 

반응형

'개발 > java' 카테고리의 다른 글

[Java] JPA란?  (1) 2026.03.20
[Java] 게시판 만들기 - 3  (0) 2025.09.14
[Java] 게시판 만들기 - 2  (0) 2025.09.08
[Java] 게시판 만들기 - 0  (0) 2025.09.07
[Java] main method 분석  (2) 2025.08.08
'개발/java' 카테고리의 다른 글
  • [Java] 게시판 만들기 - 3
  • [Java] 게시판 만들기 - 2
  • [Java] 게시판 만들기 - 0
  • [Java] main method 분석
danieLee
danieLee
개발일지
  • danieLee
    Code log
    danieLee
  • 전체
    오늘
    어제
    • 분류 전체보기 (77)
      • 개발 (76)
        • C++ (3)
        • java (6)
        • JavaScript (9)
        • python (0)
        • AWS (2)
        • Docker (6)
        • git (0)
        • 백엔드 (4)
        • Spring (7)
        • Django (2)
        • AI (3)
        • 코테 준비 (13)
        • 알고리즘 (6)
        • SKALA 4기 (13)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백엔드
    서버
    4기
    대학생
    개념
    프로그래머스
    개발자
    java
    spring
    skala
    코테
    Ai
    JavaScript
    파이썬
    js
    알고리즘
    프론트
    vue.js
    개발
    API
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.1
danieLee
[Java] 게시판 만들기 - 1
상단으로

티스토리툴바