풀스택
점프 투 스프링
혼자 만들어보기
Spring Boot Q&A 게시판 프로젝트 개발 일지
기술 스택
| 분류 | 기술 |
|---|
| 백엔드 프레임워크 | Spring Boot 4.0.7 |
| 언어 | Java 21 |
| ORM | Spring Data JPA (Hibernate) |
| 데이터베이스 | H2 (인메모리 / 파일 DB) |
| 템플릿 엔진 | Thymeleaf |
| 빌드 도구 | Gradle |
| 기타 | Lombok, MyBatis |
프로젝트 구조
src/main/java/com/example/sdd/
├── SddApplication.java # 메인 + 샘플 데이터 자동 생성
├── MainController.java # 루트("/") 리다이렉트
├── DataNotFoundException.java # 커스텀 예외
├── question/
│ ├── Question.java # 질문 엔티티
│ ├── QuestionRepository.java # JPA 레포지토리
│ ├── QuestionService.java # 비즈니스 로직
│ └── QuestionController.java # 요청 처리
└── answer/
├── Answer.java # 답변 엔티티
├── AnswerRepository.java # JPA 레포지토리
├── AnswerService.java # 비즈니스 로직
└── AnswerController.java # 요청 처리
src/main/resources/
├── application.properties # DB, JPA 설정
└── templates/
├── question_list.html # 질문 목록 페이지
└── question_detail.html # 질문 상세 + 답변 페이지
DB / JPA 설정
# application.properties
# H2 콘솔 활성화
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# H2 파일 DB (재시작해도 데이터 유지)
spring.datasource.url=jdbc:h2:~/local;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
server.port=8082
엔티티 설계
Question (질문)
java
@Data
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject; // 제목
@Column(columnDefinition = "TEXT")
private String content; // 내용
private LocalDateTime createDate; // 작성일시
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList; // 답변 목록 (1:N)
}
Answer (답변)
java
@Data
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content; // 답변 내용
private LocalDateTime createDate; // 작성일시
@ManyToOne
private Question question; // 연결된 질문 (N:1)
}
관계: Question ↔ Answer = 1:N 양방향 매핑
주요 기능 구현
1. 질문 목록 (GET /question/list)
java
@GetMapping("/list")
public String list(Model model) {
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
html
<tr th:each="question : ${questionList}">
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
2. 질문 상세 (GET /question/detail/{id})
java
@GetMapping("/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
3. 답변 등록 (POST /answer/create/{id})
java
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@RequestParam(value = "content") String content) {
Question question = this.questionService.getQuestion(id);
this.answerService.create(question, content);
return String.format("redirect:/question/detail/%s", id);
}
html
<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<ul>
<li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
</ul>
<form th:action="@{|/answer/create/${question.id}|}" method="post">
<textarea name="content" id="content" rows="15"></textarea>
<input type="submit" value="답변등록">
</form>
4. 앱 시작 시 샘플 데이터 자동 생성
java
@Bean
CommandLineRunner init(QuestionRepository questionRepository) {
return args -> {
if (questionRepository.count() > 0) return; // 중복 방지
Question q1 = new Question();
q1.setSubject("sbb가 무엇인가요?");
q1.setContent("sbb에 대해서 알고 싶습니다.");
q1.setCreateDate(LocalDateTime.now());
questionRepository.save(q1);
Question q2 = new Question();
q2.setSubject("스프링부트 모델 질문입니다.");
q2.setContent("id는 자동으로 생성되나요?");
q2.setCreateDate(LocalDateTime.now());
questionRepository.save(q2);
};
}
트러블슈팅
문제 1 - 게시글이 목록에 안 보임
- 원인: DB에 데이터가 없었음.
ddl-auto=update는 테이블만 생성하고 데이터는 안 넣음
- 해결:
SddApplication.java에 CommandLineRunner 빈을 추가하여 앱 시작 시 샘플 데이터 자동 삽입
문제 2 - 답변 폼이 보이지 않음
- 원인:
answerList의 JPA 기본 로딩 방식은 LAZY(지연 로딩)
서비스 메서드가 종료되면 JPA 세션도 닫히는데, 그 이후 Thymeleaf가 answerList에 접근하려 하면 예외 발생 → 이후 HTML이 렌더링 안 됨
- 해결:
QuestionService.getQuestion()에 @Transactional 어노테이션 추가 + answerList.size() 강제 초기화
java
@Transactional
public Question getQuestion(Integer id) {
Optional<Question> question = this.questionRepository.findById(id);
if (question.isPresent()) {
question.get().getAnswerList().size(); // 강제 초기화
return question.get();
} else {
throw new DataNotFoundException("question not found");
}
}
URL 목록
| URL | 메서드 | 설명 |
|---|
/ | GET | 질문 목록으로 리다이렉트 |
/question/list | GET | 질문 목록 |
/question/detail/{id} | GET | 질문 상세 + 답변 목록 |
/answer/create/{id} | POST | 답변 등록 |
/h2-console | GET | H2 DB 콘솔 (개발용) |
실행 방법
bash
# 프로젝트 루트에서
./gradlew bootRun
# 접속
http://localhost:8082