프로젝트 설계 부분은 이전 게시글을 참고하자.
[Java] 게시판 만들기 - 0
백엔드 공부용으로 CRUD 기능을 갖는 아주 간단한 게시판을 만들어보고자 한다. 본격적인 개발에 앞서 기본적인 서비스의 요구사항부터 정의해보자. 1. 요구사항로그인은 생략(나중에 추가) 메인
daniellee09.tistory.com
설계해둔 Class Diagram을 참고하여 Bottom-up 방식으로 개발을 진행하고자 한다. 그 이유는 다음과 같다.
- 의존성 방향: 상위 계층이 하위 계층에 의존하므로 하위부터 구현
- 테스트 용이성: 각 계층 완성 후 즉시 테스트 가능
- 안정성: 기반이 되는 부분부터 견고하게 구축
가장 먼저 프로젝트의 초기 설정부터 진행해보자.
1. 초기 설정
우선 Spring Initializer를 통해 프로젝트를 생성한다.
아래처럼 메타데이터와 의존성을 설정했다.
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);
}
정적 팩토리 메서드 패턴을 사용한 이유는 다음과 같다.
- 이름을 따로 가질 수 있다: createBoard와 같이 생성 의도가 명확해진다.
- 생성 전 검증 가능: 유효성 검사 후 생성
- 캐싱 가능: 필요시 인스턴스 재사용
- 하위 타입 반환 가능: 유연한 설계
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을 설계하면서 다음과 같은 원칙들을 최대한 고려했다.
- 캡슐화
- setter 없음 -> 무분별한 수정 방지
- 업데이트는 의미있는 메서드로만 가능
- 불변성 추구
- 생성 후 ID, 생성일시는 변경 불가
- 명시적인 업데이트 메서드로만 수정 가능
- 도메인 주도 설계(DDD)
- 엔티티가 자신의 규칙을 관리
- 비즈니스 로직이 도메인 객체에 모여있음
- 계층 분리
- 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 |