목차
지난 포스트에서는 VSCode로 Spring 게시판 만들기 프로젝트를 세팅하면서 PostgreSQL 데이터베이스까지 연동해 보았습니다. 빈 게시판이라 리스트 페이지는 만들어졌지만 아직 게시물이 하나도 없어서 썩 재미가 없었죠. 이번에는 드디어 새 게시물 작성 페이지를 구현해서 실제로 글을 등록해 볼 시간입니다! 😀 데이터베이스 연결을 마쳤으니 이제 사용자가 웹 화면에서 새 게시물을 작성하고, 저장하고, 저장 성공 메시지도 확인할 수 있게 목표를 잡았습니다. (물론 개발자라면 “드디어 CRUD의 꽃, Create 기능을 붙이는구나!” 하고 흐뭇해할지도 모릅니다.) 그럼 새 게시물 페이지 구현을 시작해 볼까요?
※ 이전글
2025.07.17 - [IT 트렌드/알아두면 좋은 IT 상식] - VSCode Java Spring 프로젝트: Maven 스프링 부트 예제 설정부터 디버깅까지
VSCode Java Spring 프로젝트: Maven 스프링 부트 예제 설정부터 디버깅까지
목차소개 (Introduction)VS Code(비주얼 스튜디오 코드) 환경에서 Java Spring Boot 프로젝트를 생성하고 실행해 본 후기입니다. 이번 포스트에서는 Spring Initializr를 통해 Maven 기반 Spring Boot HelloWorld 프로젝
kberry.tistory.com
2025.07.23 - [IT 트렌드/알아두면 좋은 IT 상식] - Java Spring Boot로 Maven 빌드 및 배포하기: 초보자도 겁내지 마세요!
Java Spring Boot로 Maven 빌드 및 배포하기: 초보자도 겁내지 마세요!
목차 안녕하세요! 오늘은 Java Spring Boot를 통해 Maven으로 빌드하고 배포하는 과정을 처음 접하는 개발자분들을 위한 가이드를 준비했습니다. 처음 시작하는 분들은 환경 설정부터 배포까지 막막
kberry.tistory.com
새 게시물 기능 구현 흐름
새 게시물 작성 기능을 만들기 위한 전체 흐름을 간단히 살펴보겠습니다. 먼저 PostController에 새 게시물 작성을 처리하는 메서드를 추가하고, 그에 대응하는 post_form.html 템플릿을 작성합니다. 사용자가 글을 쓰고 제출하면 해당 데이터를 데이터베이스에 저장하고, 리다이렉션으로 다시 홈 페이지로 돌아가게 처리할 겁니다. 홈 페이지에서는 방금 등록한 게시물이 게시물 리스트에 보이고, 화면 상단에는 “성공적으로 작성되었습니다!” 같은 성공 메시지도 표시되도록 해보겠습니다. 이제 각 단계를 차례로 구현해 보죠!
Source Path
modern-spring-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── modernspringdemo/
│ │ │ ├── ModernSpringDemoApplication.java (메인 애플리케이션 클래스)
│ │ │ ├── Post.java ( 1) 개발 post.java)
│ │ │ ├── PostController.java ( 2) 개발 PostController.java)
│ │ │ └── PostRepository.java ( 3) 개발 PostRepository.java)
│ │ └── resources/
│ │ ├── application.properties (또는 application.yml)
│ │ ├── static/ (CSS, JS, 이미지 등 정적 파일)
│ │ └── templates/ (Thymeleaf HTML 파일)
│ │ ├── home.html (Thymeleaf HTML 파일)
│ │ ├── post_form.html (Thymeleaf HTML 파일)
│ │ └── post_detail.html (Thymeleaf HTML 파일)
│ └── test/
│ └── java/
└── pom.xml
PostController에 새 게시물 메서드 추가
게시물 작성을 위해 Controller에 두 가지 메서드를 추가해야 합니다. 하나는 새 게시물 작성 폼을 보여주는 GET 메서드, 다른 하나는 폼 데이터를 받아 DB에 저장하는 POST 메서드입니다. PostController.java에 아래와 같이 메서드를 구현해 보았습니다:
// PostController.java (일부 발췌)
@Controller
public class PostController {
// ... 기존 코드 (PostRepository 주입, 홈 목록 보여주는 메서드 등) ...
// --- 새 게시물 작성 기능 추가 ---
@GetMapping("/posts/new") // 새 게시물 작성 폼 요청
public String showCreateForm(Model model) {
model.addAttribute("post", new Post()); // 빈 Post 객체를 모델에 담아 전달
return "post_form"; // post_form.html 뷰를 반환
}
@PostMapping("/posts") // 새 게시물 폼 제출 처리
public String createPost(@ModelAttribute Post post, RedirectAttributes redirectAttributes) {
postRepository.save(post); // 폼에서 받은 게시물 데이터를 DB에 저장
redirectAttributes.addFlashAttribute("message", "게시물이 성공적으로 작성되었습니다!");
return "redirect:/"; // 저장 후 홈 페이지로 리다이렉트
}
// --- 게시물 상세 보기 기능 (다음 단계에서 활용 예정) ---
@GetMapping("/posts/{id}")
public String showPostDetail(@PathVariable Long id, Model model) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid post Id:" + id));
model.addAttribute("post", post);
return "post_detail";
}
}
위 코드에서 눈여겨볼 부분을 짚어보겠습니다:
- @GetMapping("/posts/new"): 사용자가 새 게시물 작성 페이지에 접근하면 이 메서드가 실행됩니다. Model에 new Post() 빈 객체를 post라는 이름으로 담아 전달하는데요, 이렇게 해야 템플릿에서 폼과 객체를 바인딩할 수 있습니다. (Thymeleaf 폼 바인딩을 사용하기 위함이에요.)
- @PostMapping("/posts"): 사용자가 폼을 제출하면 /posts로 POST 요청이 들어와 이 메서드가 처리합니다. @ModelAttribute Post post 덕분에 폼에서 제출된 제목(title)과 내용(content)이 자동으로 Post 객체에 바인딩됩니다. 미리 연결해 둔 PostRepository를 통해 postRepository.save(post)로 새 게시물을 데이터베이스에 저장합니다.
이때 Post 엔티티의 작성 시각(createdAt)은 미리 엔티티에 설정해 둔 @PrePersist 어노테이션 덕분에 자동으로 기록되게 했습니다. 저장이 완료되면 리다이렉트 속성(RedirectAttributes)을 이용해 "message"를 플래시(Flash) 속성으로 추가합니다. "게시물이 성공적으로 작성되었습니다!"라는 문구를 실어 보냈죠. 마지막으로 return "redirect:/"를 반환하여 홈 페이지로 리다이렉트 합니다. 이렇게 하면 새 게시물 작성 후 사용자가 자동으로 메인 화면으로 돌아가게 됩니다. - Flash Attribute 활용: 여기서 RedirectAttributes.addFlashAttribute()를 사용한 점이 중요합니다. 그냥 model.addAttribute로 메시지를 담고 리다이렉트 하면 새 요청에서는 데이터가 사라져 버립니다. Flash attribute를 쓰면 리다이렉트 이후에도 한 번 사용할 데이터(성공 메시지)를 전달할 수 있어요. 덕분에 리다이렉트 된 페이지에서 사용자에게 성공 메시지를 보여줄 수 있습니다.
- (참고) @GetMapping("/posts/{id}"): 위 코드 마지막 부분에는 게시물 상세 보기 기능을 위한 메서드도 미리 추가해 두었습니다. 아직 상세 페이지 뷰(post_detail.html)는 구현하지 않았지만, 다음 단계에서 게시물 상세 보기나 수정/삭제 기능을 넣을 때 사용할 예정입니다. 😉 이번 포스트에서는 우선 작성 페이지에 집중할게요.
Post, PostRepository에 새 게시물 메서드 추가
게시물 작성을 위해 Controller에 두 가지 메서드를 추가해야 합니다. 하나는 새 게시물 작성 폼을 보여주는 GET 메서드, 다른 하나는 폼 데이터를 받아 DB에 저장하는 POST 메서드입니다. PostController.java에 아래와 같이 메서드를 구현해 보았습니다:
1) Post.java
// src/main/java/com/example/modernspringdemo/Post.java
package com.example.modern_spring_demo;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity // 이 클래스가 JPA 엔티티임을 나타냅니다. 데이터베이스 테이블과 매핑됩니다.
@Table(name = "posts") // 매핑될 데이터베이스 테이블 이름을 "posts"로 지정합니다.
@Getter // Lombok: 모든 필드에 대한 Getter 메소드를 자동으로 생성합니다.
@Setter // Lombok: 모든 필드에 대한 Setter 메소드를 자동으로 생성합니다.
@NoArgsConstructor // Lombok: 인자 없는 기본 생성자를 자동으로 생성합니다.
@AllArgsConstructor // Lombok: 모든 필드를 인자로 받는 생성자를 자동으로 생성합니다.
public class Post {
@Id // 이 필드가 테이블의 기본 키(Primary Key)임을 나타냅니다.
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 데이터베이스에 위임합니다 (PostgreSQL의 SERIAL/BIGSERIAL).
private Long id; // 게시물 ID
@Column(nullable = false, length = 255) // 컬럼 제약조건: NULL 불가능, 최대 길이 255
private String title; // 게시물 제목
// @Lob // Large Object. 텍스트 길이가 길어질 수 있음을 나타냅니다 (TEXT 타입으로 매핑될 수 있음).
// @Column(nullable = false)
// private String content; // 게시물 내용
@Column(nullable = false, columnDefinition = "TEXT") // 혹은 생략 가능
private String content;
@Column(nullable = false, updatable = false) // 생성 시간은 NULL 불가능, 업데이트 불가능
private LocalDateTime createdAt; // 게시물 생성 시간
// 엔티티가 영속화되기 전에 자동으로 호출되는 메소드 (생성 시간을 자동으로 설정)
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
}
2) PostController.java
// src/main/java/com/example/modernspringdemo/PostController.java
package com.example.modern_spring_demo;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; // ModelAttribute 어노테이션 추가
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; // PostMapping 어노테이션 추가
import org.springframework.web.servlet.mvc.support.RedirectAttributes; // RedirectAttributes 추가 (성공 메시지 전달용)
@Controller // 이 클래스가 Spring MVC 컨트롤러임을 나타냅니다.
public class PostController {
private final PostRepository postRepository; // PostRepository를 주입받아 사용합니다.
// 생성자 주입 (스프링이 PostRepository 구현체를 자동으로 찾아서 넣어줍니다)
@Autowired // Spring이 의존성 주입을 할 때 사용합니다.
public PostController(PostRepository postRepository) {
this.postRepository = postRepository;
}
@GetMapping("/") // 루트 URL ("/")로 GET 요청이 들어오면 이 메소드가 처리합니다.
public String home(Model model) {
// 데이터베이스에서 모든 게시물을 최신순으로 가져옵니다.
List<Post> posts = postRepository.findAllByOrderByCreatedAtDesc();
// 가져온 게시물 목록을 "posts"라는 이름으로 모델에 추가합니다.
// 이 "posts"는 home.html 템플릿에서 사용할 수 있습니다.
model.addAttribute("posts", posts);
// "home"이라는 뷰 이름을 반환합니다. Spring은 이 이름으로 templates 폴더에서 home.html 파일을 찾습니다.
return "home";
}
// --- 새 게시물 작성 기능 추가 ---
@GetMapping("/posts/new") // GET 요청: 새 게시물 작성 폼을 보여줍니다.
public String showCreateForm(Model model) {
model.addAttribute("post", new Post()); // 비어있는 Post 객체를 폼에 바인딩할 모델에 추가합니다.
return "post_form"; // templates/post_form.html 파일을 렌더링합니다.
}
@PostMapping("/posts") // POST 요청: 폼 데이터를 받아 새 게시물을 저장합니다.
public String createPost(@ModelAttribute Post post, RedirectAttributes redirectAttributes) {
// Post 객체는 폼에서 전송된 title과 content 값으로 채워져 있습니다.
// createdAt은 @PrePersist 어노테이션에 의해 자동으로 설정됩니다.
postRepository.save(post); // 데이터베이스에 게시물을 저장합니다.
// 게시물 저장 성공 메시지를 리다이렉션 후에도 사용할 수 있도록 Flash Attribute로 추가합니다.
redirectAttributes.addFlashAttribute("message", "게시물이 성공적으로 작성되었습니다!");
// 게시물 목록 페이지로 리다이렉트합니다.
return "redirect:/";
}
// --- 게시물 상세 보기 기능 (미리 추가) ---
@GetMapping("/posts/{id}") // GET 요청: 특정 ID의 게시물 상세 내용을 보여줍니다.
public String showPostDetail(@PathVariable Long id, Model model) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid post Id:" + id));
model.addAttribute("post", post);
return "post_detail"; // templates/post_detail.html 파일을 렌더링합니다.
}
}
3) PostRepository.java
// src/main/java/com/example/modernspringdemo/PostRepository.java
package com.example.modern_spring_demo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// JpaRepository를 상속받아 기본적인 CRUD 기능을 제공받습니다.
// <엔티티 클래스, 엔티티의 ID 타입>
@Repository // 이 인터페이스가 Spring Bean으로 등록되도록 합니다. (선택 사항이지만 명시하는 것이 좋음)
public interface PostRepository extends JpaRepository<Post, Long> {
// 여기에 필요한 추가적인 쿼리 메소드를 정의할 수 있습니다.
// 예: List<Post> findByTitleContaining(String title);
List<Post> findAllByOrderByCreatedAtDesc();
}
post_form.html 작성 및 바인딩 처리
컨트롤러가 준비되었으니 이제 새 게시물 작성 폼을 만들어봅시다. src/main/resources/templates/ 폴더에 post_form.html 파일을 생성하고 아래와 같이 내용을 작성했습니다. Thymeleaf를 사용하여 서버에서 전달한 post 객체와 폼을 바인딩할 것입니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>새 게시물 작성</title>
<style>
/* 간단한 스타일 설정 */
body { background-color: #f4f4f4; font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 600px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; }
h1 { text-align: center; color: #333; }
form div { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
textarea { resize: vertical; }
.back-link { display: block; margin-top: 10px; text-align: center; text-decoration: none; color: #666; }
</style>
</head>
<body>
<div class="container">
<h1>새 게시물 작성</h1>
<form th:action="@{/posts}" th:object="${post}" method="post">
<div>
<label for="title">제목:</label>
<input type="text" id="title" th:field="*{title}" placeholder="제목을 입력하세요" required />
</div>
<div>
<label for="content">내용:</label>
<textarea id="content" th:field="*{content}" placeholder="내용을 입력하세요" required></textarea>
</div>
<button type="submit">게시물 작성</button>
</form>
<a href="/" class="back-link">메인 페이지로 돌아가기</a>
</div>
</body>
</html>
위 템플릿의 핵심은 Thymeleaf 폼 바인딩입니다. th:object="${post}"를 <form> 태그에 지정하면 이 폼이 Post 객체와 연동된다는 것을 의미합니다. 그리고 각 입력 필드에는 th:field="*{title}"과 th:field="*{content}"를 사용하여 Post 객체의 title, content 속성과 폼 입력값을 연결했습니다. 이렇게 하면 폼을 제출할 때 자동으로 Post 객체에 값이 채워져서 컨트롤러의 @ModelAttribute Post post 파라미터로 넘어오게 됩니다.
th:action="@{/posts}" method="post"로 폼의 제출 경로를 /posts로 지정한 것도 확인하세요. 이 설정과 우리가 앞서 컨트롤러에 정의한 @PostMapping("/posts")가 서로 매칭되어 폼 전송 시 해당 메서드가 호출되는 거죠. 제목과 내용 두 필드는 required 속성을 줘서 빈 값으로 제출되지 않도록 간단한 HTML5 검증도 추가했습니다.
이제 브라우저에서 새 게시물 작성 페이지를 열면 멋진 폼이 나타날 것입니다. 타이틀과 내용을 입력하고 "게시물 작성" 버튼을 누를 준비가 되었어요! 😆 아래는 완성된 새 게시물 작성 화면의 모습입니다.

데이터 저장 및 리다이렉션 처리
폼에서 데이터를 제출하면 어떻게 동작하는지 흐름을 다시 짚어볼까요? 사용자가 "게시물 작성" 버튼을 누르면, 폼이 /posts로 POST 요청을 보냅니다. 이 요청은 PostController의 createPost() 메서드로 전달되어 우리가 작성한 대로 postRepository.save(post)가 실행됩니다. 데이터베이스 테이블에 새로운 게시물이 한 줄 추가되는 순간이죠! 🚀
저장이 성공하면, 컨트롤러는 RedirectAttributes를 통해 성공 메시지를 설정하고 redirect:/ 응답을 보냅니다. 이때 브라우저는 / (홈 페이지 URL)로 다시 요청하게 됩니다(리다이렉트). 중요한 것은, 우리가 Flash attribute로 넣었던 "message"가 이 새로운 요청까지 전달된다는 점입니다. 덕분에 홈 페이지 컨트롤러(home() 메서드)는 모델에 "posts" 리스트뿐 아니라 "message"도 있게 되고, 뷰에서 이를 사용할 수 있게 됩니다.
결과적으로 사용자는 폼 제출 → 저장 → 홈으로 이동이라는 일련의 흐름 속에서 끊김 없이 경험을 이어갈 수 있어요. 만약 리다이렉트를 쓰지 않고 바로 폼 페이지를 다시 렌더링 했다면, F5 새로고침 시 중복 등록되는 문제 등이 있었을 겁니다. 리다이렉트 패턴을 통해 이러한 새로고침 이슈도 예방하고, URL도 /로 깨끗하게 유지할 수 있습니다. (PRG Pattern이라고도 하죠 – Post/Redirect/Get 패턴!)
요약하면, 사용자 입장에서는 "글 작성 → 제출 클릭 → 메인 화면에 내 글과 성공 메시지가 보임"으로 느껴지고, 개발자 입장에서는 "폼 입력받기 → DB 저장 → 리다이렉트로 새 요청 처리" 순서로 동작하는 것입니다.
이제 진짜 글이 잘 저장되는지 확인하기 위해 직접 애플리케이션을 실행해 볼 차례입니다. (mvn clean spring-boot:run으로 서버를 실행했다면, 브라우저에서 http://localhost:8080/ 를 새로고침해보세요.)
home.html 수정하여 목록과 메시지 표시
새 게시물을 작성한 후 사용자가 보게 될 홈 화면을 개선해야 합니다. 이전 단계까지는 home.html에서 게시물 목록만 뿌려주고 있었는데요, 여기서 몇 가지 업데이트를 해보겠습니다: 새 글 작성 성공 메시지를 화면에 표시하고, 새 게시물 작성 버튼도 메인 페이지에 추가하여 언제든 글쓰기 페이지로 갈 수 있도록 하는 것이죠. 그리고 데이터베이스에 게시물이 없는 경우엔 “아직 게시물이 없습니다”라는 문구도 보여주면 UX에 좋겠죠.
먼저, 게시물 리스트 렌더링 부분은 기존과 동일하게 유지하되, 맨 위에 성공 메시지를 보여주는 코드를 추가합니다. 또 목록 상단에 "새 게시물 작성" 버튼을 하나 넣어서 사용자들이 메인 화면에서 바로 글쓰기 페이지로 이동할 수 있게 해보았습니다. 수정된 home.html의 주요 부분은 다음과 같습니다:
<!-- home.html (일부 발췌) -->
<body>
<div class="container">
<h1>나의 블로그</h1>
<!-- 1. 성공 메시지 출력 -->
<div th:if="${message}" class="message">
<p th:text="${message}"></p>
</div>
<!-- 2. 새 게시물 작성 버튼 -->
<a href="/posts/new" class="create-button">새 게시물 작성</a>
<!-- 3. 게시물 리스트 출력 -->
<div th:if="${posts.isEmpty()}" class="no-posts">
<p>아직 작성된 게시물이 없습니다.</p>
</div>
<div th:unless="${posts.isEmpty()}">
<div th:each="post : ${posts}" class="post">
<h2 th:text="${post.title}">게시물 제목</h2>
<div class="post-meta">
<span th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}">2025-01-01 12:00</span>
</div>
<div class="post-content">
<p th:text="${post.content}">본문 내용</p>
</div>
<a th:href="@{/posts/{id}(id=${post.id})}">자세히 보기</a>
</div>
</div>
</div>
</body>
코드를 보시면 <div th:if="${message}"> 부분이 새로 추가된 성공 메시지 영역입니다. 컨트롤러에서 Flash attribute로 넘긴 message가 있을 경우에만 이 <div>가 렌더링되고, <p th:text="${message}"></p>를 통해 메시지 문구가 화면에 표시됩니다. CSS에서 .message { color: green; text-align: center; } 등으로 스타일을 주어서 성공 메시지는 녹색 글씨로 중앙에 나타나도록 했습니다. (사용자가 한눈에 알아볼 수 있게요!)
그 아래 <a href="/posts/new" class="create-button">새 게시물 작성</a> 링크가 보이죠? 이것이 메인 화면에 추가한 글쓰기 버튼입니다. 버튼을 누르면 우리가 만든 /posts/new 경로로 이동하여 새 글 작성 폼 페이지로 넘어가게 됩니다. style 클래스로. create-button을 정의해서 초록색 배경의 예쁜 버튼 형태로 꾸며두었습니다.
마지막으로 게시물 목록 출력 부분은 이전과 동일하지만 개선한 점이 하나 있습니다. th:if="${posts.isEmpty()}" 블록을 사용해 게시물이 하나도 없을 때는 친절하게 안내 문구를 보여주도록 했습니다. 반대로 게시물이 존재하면 th:unless="${posts.isEmpty()}" 영역이 렌더링 되어, th:each="post : ${posts}"로 리스트를 순회하며 제목, 작성일시, 내용 일부, "자세히 보기" 링크를 표시합니다. (post.content가 너무 길 경우 #strings. substring(...)으로 200자까지만 잘라서 미리 보기로 보여주는 처리도 해두었어요. 전체 내용은 "자세히 보기"로 상세 페이지에서 보게 만들 계획입니다.)
이제 새 글을 한 번 등록해 보고 홈 화면이 어떻게 바뀌는지 살펴볼까요? 처음에는 “아직 작성된 게시물이 없습니다.”라는 문구만 있던 빈 화면이, 글을 작성한 후에는 목록에 새로운 게시물이 나타나고, 상단에는 성공 메시지도 뜨게 됩니다! 아래는 게시물 작성 후 메인 화면의 모습입니다.

post_form.html, post_detail.html 새 게시물 view
1) post_form.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>새 게시물 작성</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
}
.container {
max-width: 600px;
margin: auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
}
form div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
textarea {
width: calc(100% - 20px); /* 패딩 고려 */
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
}
textarea {
resize: vertical; /* 세로 크기 조절 가능 */
min-height: 150px;
}
button {
display: block;
width: 100%;
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
font-size: 1em;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.back-link {
display: block;
text-align: center;
margin-top: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>새 게시물 작성</h1>
<form th:action="@{/posts}" th:object="${post}" method="post">
<div>
<label for="title">제목:</label>
<input type="text" id="title" th:field="*{title}" required />
</div>
<div>
<label for="content">내용:</label>
<textarea id="content" th:field="*{content}" required></textarea>
</div>
<button type="submit">게시물 작성</button>
</form>
<a href="/" class="back-link">메인 페이지로 돌아가기</a>
</div>
</body>
</html>
2) post_detail.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title th:text="${post.title}">게시물 상세</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f8f8;
padding: 20px;
}
.container {
max-width: 800px;
margin: auto;
background: white;
padding: 20px;
border-radius: 8px;
}
h1 {
color: #333;
}
.meta {
color: #888;
margin-bottom: 20px;
}
.content {
line-height: 1.6;
white-space: pre-line;
}
a {
display: inline-block;
margin-top: 20px;
text-decoration: none;
color: #007bff;
}
</style>
</head>
<body>
<div class="container">
<h1 th:text="${post.title}">게시물 제목</h1>
<div class="meta">
<span th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"
>작성일</span
>
</div>
<div class="content" th:text="${post.content}">게시물 내용</div>
<a href="/">← 메인으로 돌아가기</a>
</div>
</body>
</html>
새 게시물 작성까지 완료(성공메시지)

개발자들이 흔히 놓치는 포인트 (유머 섞기)
프로젝트를 진행하면서 놓치기 쉬운 부분이나 삽질 포인트를 한 번 짚어볼까요? 너무 진지한 내용만 있어도 머리가 아프니, 개발자분들이 고개 끄덕일만한 얘기들로 잠시 웃어보겠습니다. 😉
- Flash Attribute 까먹었을 때: 분명 컨트롤러에서 성공 메시지를 모델에 넣은 것 같은데, 리다이렉트 뒤에 안 나와서 당황한 경험 있으신가요? 알고 보니 RedirectAttributes를 안 쓰고 그냥 model.addAttribute를 썼더라... 😅 리다이렉트에서는 모델이 초기화된다는 사실, 한 번쯤 깜빡하기 쉽죠. (성공인 줄 알았던 메시지가 안 보여서 머리가 띵~) 이제부터는 "리다이렉트 시엔 flash!" 꼭 기억해요.
- URL 매핑 오타 지뢰: 개발 속도에 쫓겨 오타를 하나 냈을 뿐인데 앱이 말을 안 듣습니다. 예를 들어, 컨트롤러는 /posts로 받아야 하는데 폼 액션을 /post로 적는다든지, @GetMapping("/new")라고 쓰고 실제 요청은 /posts/new로 보내버린다든지 하는 실수요. 이러면 Spring Boot는 친절하게(?) Whitelabel Error Page를 보여주는데, 원인을 모를 땐 한참 헤매게 되죠. 결국 알고 보면 Controller 오타 하나에 반나절 날아가는 마법... 한 번쯤 경험해 보셨을 거예요. 😅 (제 콘솔 로그랑 Stack Overflow 검색 히스토리는 그 증인입니다.)
- DB 연결 오류 대소동: 설정 파일에 DB 비밀번호를 틀리게 넣었거나, DB 서버를 실행 안 한 채로 mvn spring-boot:run을 하면 로그에 오류가 주르륵 뜨면서 접속이 안 됩니다. 하필 이런 DB 에러는 왜 꼭 발표 전날에 터지냐고요! 😂 이럴 땐 침착하게 application.properties의 DB 설정, 아이디/비번, 그리고 pgAdmin으로 DB 서버 올라와 있는지 다시 확인합시다. 개발자라면 “어제 잘 되던 게 오늘 오류 난다” 상황을 피할 순 없지만... 당황하지 말고 원인부터 차근차근 찾으면 됩니다. (깜빡하고 PostRepository에 @Query 하나 잘못 써놨다든가 하는 사소한 이유일 수도 있거든요.)
- 기타 흔한 실수들: 그 외에도 @Entity나 @Table 어노테이션 빼먹어서 테이블 생성이 안 된다든가, Thymeleaf 템플릿 파일 이름 오타로 “템플릿을 찾을 수 없습니다” 에러가 난다든가 하는 실수도 흔합니다. 사람은 누구나 실수하니, 에러가 발생하면 스택트레이스를 차근차근 읽어보고 구글신의 도움을 받아가며 하나씩 해결해 봅시다. 삽질을 통해 우리는 또 성장하니까요! 💪
※ 다음글
2025.07.29 - [IT 트렌드/알아두면 좋은 IT 상식] - Git 없이 S3로 구축하는 AWS DevOps 배포 파이프라인
Git 없이 S3로 구축하는 AWS DevOps 배포 파이프라인
목차전체 아키텍처 개요Git 저장소를 사용하지 않고도 AWS 서비스만으로 CI/CD 파이프라인을 구성할 수 있습니다. 이번 구성에서는 Amazon S3 버킷을 코드 패키지 저장소로 사용하고, AWS CodeDeploy를 통
kberry.tistory.com
2025.07.31 - [IT 트렌드/알아두면 좋은 IT 상식] - AWS CodePipeline에서 GitHub 연동 – 실무 후기 및 CI/CD 구축 가이드
AWS CodePipeline에서 GitHub 연동 – 실무 후기 및 CI/CD 구축 가이드
목차GitHub에 있는 코드를 AWS CodePipeline으로 빌드하고 배포하는 CI/CD 파이프라인을 구축해보았습니다. 이번 포스트에서는 “AWS CodePipeline에서 GitHub 연동”하는 방법을 실무 관점에서 공유하고, 자
kberry.tistory.com
AWS Elastic Beanstalk를 활용한 Spring Boot 자동 배포: CodePipeline 연동 가이드
목차 AWS Elastic Beanstalk, Spring Boot 자동 배포, CodePipeline 연동 – 이 세 가지 키워드로 대표되는 이번 가이드에서는 Spring Boot 애플리케이션을 AWS 클라우드에 자동 배포하는 방법을 단계별로 알아봅
kberry.tistory.com
Summary & Conclusion
이번 포스트에서는 Spring 게시판 프로젝트에 새 게시물 작성 페이지를 구현하여, 사용자가 웹에서 직접 새 글을 올리고 확인할 수 있도록 만들었습니다. VSCode 환경에서 Spring Boot, Thymeleaf, JPA를 활용해 폼 -> 저장 -> 리다이렉트 -> 리스트 갱신이라는 일련의 흐름을 다뤄보니, 이제 비로소 우리 게시판이 실제 동작하는 웹 애플리케이션처럼 느껴지지 않나요? 😃
핵심을 다시 정리하면 다음과 같습니다:
- Controller: PostController에 새 게시물 작성 폼을 띄우는 메서드와 게시물 저장을 처리하는 메서드를 추가했다. 폼 표시 메서드는 빈 Post 객체를 모델에 담아 템플릿에 전달했고, 저장 메서드는 @ModelAttribute로 폼 데이터를 받아 DB에 저장 후 홈으로 리다이렉트 했다.
- Template: post_form.html을 만들어 Thymeleaf 폼을 구성했다. th:object="${post}"와 th:field="*{...}"를 사용하여 Post 객체와 입력 폼을 바인딩했고, 사용 편의를 위해 약간의 CSS 스타일과 메인으로 돌아가는 링크도 추가했다.
- Home 화면: home.html에 성공 메시지 출력 기능과 "새 게시물 작성" 버튼을 추가했다. 게시물 리스트가 비어있을 때 안내 문구를 보여주는 처리도 해주어 사용자 경험을 개선했다. Flash attribute를 통한 성공 메시지 표시로 사용자에게 피드백을 제공했다.
이제 사용자는 자유롭게 새 글을 작성하고 볼 수 있게 되었으며, 우리 프로젝트는 한 단계 레벨 업했습니다. 🙌 이번 단계까지 따라오느라 수고 많으셨어요!
다음 시간에는 예고한 대로 게시물 수정 및 삭제 기능 구현에 도전해 보겠습니다. 📝✂️ 게시물을 잘못 올렸을 때 내용을 고치거나 삭제할 수 있어야 완벽한 게시판이 되겠죠? 또한 미리 추가해 둔 게시물 상세 보기 페이지도 완성할 예정입니다. 점점 더 쓸만한 게시판으로 발전해 가는 우리 프로젝트, 기대해 주세요! 🚀
FAQ (자주 묻는 질문)
Q1. Spring 게시판 예제를 더 찾아보고 싶어요. 이 프로젝트가 도움이 될까요?
A1. 네, 이번에 구현한 기능 자체가 Spring 게시판 예제로 손색없습니다. 😀 Spring Boot와 Thymeleaf를 사용한 기본적인 게시판 기능(글 작성, 목록 표시 등)을 다뤘기 때문에, 이 코드를 기반으로 추가 기능을 연습해 볼 수 있어요. 더 큰 예제를 원한다면 Spring 공식 예제나 오픈소스 게시판 프로젝트도 참고할 수 있겠지만, 우선은 여기서 만든 간단한 게시판으로 개념을 익힌 뒤 확장해 나가길 권장합니다. 예를 들어, 이 예제에 사용자 인증을 붙여서 로그인 기능이 있는 게시판으로 발전시켜 볼 수도 있고요. 작은 예제를 직접 만들어 본 것이 큰 프로젝트를 이해하는 밑거름이 될 거예요!
Q2. Spring 게시판 만들기 도중에 자주 발생하는 오류에는 무엇이 있을까요?
A2. 처음 Spring 게시판 만들기에 도전하면 몇 가지 흔한 오류를 만나게 됩니다. 예를 들어:
- Whitelabel Error Page : 화면에 새하얀 에러 페이지가 뜨는 건 주로 요청 경로에 매핑된 컨트롤러가 없거나, 뷰 템플릿 이름이 잘못된 경우에 발생해요. 이런 오류가 나오면 콘솔 로그를 가장 먼저 확인해 보세요. 어떤 컨트롤러나 템플릿을 못 찾았다는 에러 메시지가 있을 겁니다. 해결 방법은 대응되는 컨트롤러 메서드가 존재하는지, @GetMapping 경로 오타는 없는지, 또는 반환하는 뷰 이름과 실제 파일명이 맞는지 등을 점검하면 됩니다.
- DB 관련 오류 : JPA로 게시판을 만들 때 DB 연결 오류나 SQL 에러도 흔해요. 앞서 언급했듯 DB 설정이 잘못되면 애플리케이션이 뜨자마자 에러를 던질 거고요, 게시물 저장 시에 테이블이 없다는 오류가 난다면 엔티티와 테이블 매핑 문제가 있을 수 있습니다. 이런 경우 로그에 나오는 SQL 구문이나 오류 메시지를 보고, 엔티티 클래스에 @Entity, @Id 등이 빠지진 않았는지, application.properties의 spring.jpa.hibernate.ddl-auto 설정이 잘 되어있는지 등을 확인하세요. 또한 한글 입력 시 깨짐 같은 이슈가 있다면 데이터베이스 인코딩이나 프로젝트 인코딩 설정을 살펴봐야 합니다.
그 밖에 NullPointerException처럼 객체 주입이 안 된 경우도 종종 보이는데, 이는 필드를 private 하게 정의만 하고 생성자 주입/@Autowired를 빼먹었을 때 발생합니다. 항상 오류 메시지를 차근차근 읽어보고, 구글 검색을 통해 원인에 대한 정보를 찾아보세요. 모든 개발자가 이런 시행착오를 겪으며 성장한답니다! 파이팅! 💪
'IT 트렌드 > 알아두면 좋은 IT 상식' 카테고리의 다른 글
| 개발자의 Git 브랜치 전략 일기 (0) | 2025.10.17 |
|---|---|
| Tomcat server.xml 설정 완벽 가이드: 운영/DevOps 실무 베스트 프랙티스 (5) | 2025.08.26 |
| VS Code 우클릭 메뉴 없어서 당황했다면? 레지스트리 수정으로 5분 만에 해결! (4) | 2025.08.01 |
| Java Spring Boot로 Maven 빌드 및 배포하기: 초보자도 겁내지 마세요! (2) | 2025.07.23 |
| VSCode Java Spring 프로젝트: Maven 스프링 부트 예제 설정부터 디버깅까지 (4) | 2025.07.17 |