Fullstack 58

heo4·2026년 7월 7일

Fullstack

목록 보기
13/71

풀스택

점프 투 스프링

혼자 만들어보기

Spring Boot Q&A 게시판 프로젝트 개발 일지

기술 스택

분류기술
백엔드 프레임워크Spring Boot 4.0.7
언어Java 21
ORMSpring 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
<!-- 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
<!-- question_detail.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/listGET질문 목록
/question/detail/{id}GET질문 상세 + 답변 목록
/answer/create/{id}POST답변 등록
/h2-consoleGETH2 DB 콘솔 (개발용)

실행 방법

bash
# 프로젝트 루트에서
./gradlew bootRun

# 접속
http://localhost:8082

0개의 댓글