| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- backend
- 백엔드 회원가입 구현
- Service Layer
- docker
- JWT
- 틀린문제조회
- AWS
- Integration Test
- SolveGO
- SelfHostedRunner
- ERD설게
- MySQL
- Spring Security
- CI
- Slice Test
- 백엔드 로그인 구현
- springboot
- 테스트 코드
- 제1종오류
- 백엔드
- Spring Boot
- branch protection
- rest api
- EC2
- 책임분리
- JPA
- performance test
- query optimization
- 제2종오류
- B+Tree
- Today
- Total
노트
[SolveGO #3] 여기 설계도대로 집 지어주세요 - Spring boot setup부터 Entity Class 정의 본문
[SolveGO #3] 여기 설계도대로 집 지어주세요 - Spring boot setup부터 Entity Class 정의
forwarder 2026. 6. 19. 15:53
서론
나는 개발 과정을 하나의 건물을 짓는 과정과 비슷하다고 생각한다.
건물을 짓는 사람은 가장 먼저 건물의 목적을 정의한다.
그리고 예산, 부지 면적, 자재와 같은 사용 가능한 자원을 고려하여 그 목적을 가장 효율적으로 달성할 수 있는 설계를 고민한다.
설계가 끝나면 설계도는 현장으로 전달되고, 이제 종이 위의 청사진을 현실의 건물로 바꾸는 과정이 시작된다.

SolveGO 프로젝트 또한 MVP 범위 내에서 요구사항 분석, ERD 설계, API 설계를 모두 마친 상태이다.
이제 설계도를 실제 코드로 옮길 차례다.
이번 글에서는 Spring Boot 프로젝트를 생성하고 개발 환경을 구성하는 과정부터,
도메인의 기본 자재가 될 Entity 클래스를 정의하는 과정까지 다룬다.
건물을 짓기 전에 먼저 부지를 정비하고 필요한 자재를 준비하듯, 본격적인 기능 구현에 앞서 프로젝트의 기초 구조를 만들어 보자.
Project setup
spring initlizr는 Spring boot Project 기본 골격을 자동 생성해주기 때문에, 매우 간편하다.
환경 세팅은 아래와 같이 해주었다.

Project
Gradle과 Maven은 프로젝트에 필요한 라이브러리와 그 라이브러리가 의존하는 하위 라이브러리들까지 자동으로 다운로드하고 관리해 주는 빌드 도구이다.
Maven은 XML 기반으로 의존성을 관리하고, Gradle은 Groovy 또는 Kotlin DSL을 사용하여 의존성을 관리한다.
XML에 비해 Gradle의 DSL 문법이 더 간결하고 가독성이 좋다고 판단하여 Gradle을 선택하였다.
Language
Java에 익숙했기 때문에 Java를 선택하였다.
최근 채용 공고를 살펴보면 Kotlin을 사용하는 회사도 상당수 존재하지만, Kotlin 또한 JVM 위에서 동작하며 Java와 높은 호환성을 가진 언어이다.
따라서 Java에 대한 이해를 충분히 갖춘 뒤 Kotlin을 학습하는 것이 효율적이라고 판단하였다.
Spring Boot
글을 작성하는 시점인 2026-06-19 기준으로 Spring Boot 4.x 버전이 출시된 상태였다.
Spring Framework는 하위 호환성을 잘 유지하는 것으로 유명하지만, 최신 메이저 버전은 아직 충분한 레퍼런스와 실무 사례가 축적되지 않았을 수 있다.
또한 Spring이 의존하는 외부 라이브러리들의 호환성 문제도 발생할 가능성이 있다.
따라서 개발 과정에서 참고할 수 있는 자료와 커뮤니티 사례가 충분히 축적되어 있는 Spring Boot 3.5.x 버전을 선택하였다.
Dependency
가장 중요한 부분이다.
SolveGO는 3-Tier Web Application 구조로 개발할 예정이므로
먼저 Spring MVC 기반의 웹 애플리케이션을 구성하기 위해 Spring Web을 추가하였다.
또한 문제, 사용자, 풀이 기록과 같은 데이터를 Persisently하게 저장해야 하므로 관계형 데이터베이스(RDBMS)인 MySQL을 선택하였고, 애플리케이션이 MySQL과 통신할 수 있도록 MySQL Driver를 추가하였다.
또한 그 통신을 하는 규약인 JPA 또한 가져와 Relation <-> Object 변환(ORM)이 쉽게 이루어져 구현 난이도를 낮추고자 하였다.
마지막으로는 사용자의 입력값을 검증해야하므로 Validation을 가져왔다.
한편, Getter와 Setter 등의 보일러 플레이트 코드를 대신 작성해주는 Lombok은 의도적으로 포함하지 않았다.
지금 공부 목적도 있는 내 상황에서는 먼저 Lombok 없이 코드를 직접 작성하며 Java와 Spring이 실제로 어떤 코드를 필요로 하는지 이해한 뒤, 추후 리팩토링 과정에서 Lombok을 적용해 그 필요성과 장점을 체감해 보는 것이 더 좋은 학습 경험이 될 것이라 판단하였기 때문이다.
Connect DB
위와 같이 Spring boot 환경 설정을 마친 뒤
애플리케이션을 실행하면 아래와 같은 에러 메세지가 뜬다.
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Process finished with exit code 1
원인은 명확했다.
프로젝트에 Spring Data JPA 의존성을 추가했기 때문에 Spring Boot는 애플리케이션 시작 시 DataSource를 자동 구성하려고 하였다.
그러나 DB 접속 정보가 제공되지 않았기 때문에 Datasource 생성에 실패 했다.
해결 방법은 간단하다.
1. MySQL 서버를 실행한다.
2. 애플리케이션이 사용할 DB를 생성한다.
3. application.yaml에 DB 접속 정보를 작성한다.
먼저 Homebrew를 통해 mysql을 설치하고 실행하자.
brew services start mysql 명령은 백그라운드에서 MySQL 서버 데몬을 실행한다.
brew install mysql
brew services start mysql
mysql -u root
mysql에 접속한 뒤 사용할 DB를 생성하자.
mysql> CREATE DATABASE solvego;
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| solvego |
| sys |
+--------------------+
5 rows in set (0.004 sec)
그리고 mysql이 사용하는 포트 번호를 확인하자.
mysql> SHOW VARIABLES LIKE 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port | 3306 |
+---------------+-------+
1 row in set (0.008 sec)
여기서 mysql의 port 번호는 3306 번이므로,
spring boot의 application.yaml 파일에 url : jdbc:mysql://localhost:3306/solvego를 적어주면 된다.
이 URL은 JDBC URL 형식이다.
jdbc:<subprotocol>://<host>:<port>/<database>
구체적 의미는 아래와 같다.
jdbc → Java Database Connectivity를 사용한다.
mysql → MySQL용 JDBC Driver를 사용한다.
localhost → 현재 내 컴퓨터에서 실행 중인 DB 서버에 접속한다.
3306 → MySQL 서버가 열려 있는 포트 번호이다.
solvego → 접속할 데이터베이스 이름이다.
즉 이 설정은 Spring Boot에게
“현재 내 컴퓨터의 3306 포트에서 실행 중인 MySQL 서버의 solvego 데이터베이스에 접속하라”고 알려주는 것이다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/solvego
username: root
password:
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8080
이후 애플리케이션을 다시 실행하면 아래와 같이 HikariCP가 커넥션을 생성한 것을 알 수 있다.
2026-06-18T16:02:23.486+09:00 INFO 2469 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2026-06-18T16:02:23.507+09:00 INFO 2469 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.53.Final
2026-06-18T16:02:23.518+09:00 INFO 2469 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2026-06-18T16:02:23.609+09:00 INFO 2469 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2026-06-18T16:02:23.617+09:00 INFO 2469 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-06-18T16:02:23.754+09:00 INFO 2469 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@59a94d0f
2026-06-18T16:02:23.754+09:00 INFO 2469 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-06-18T16:02:23.786+09:00 INFO 2469 --- [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Entity Class Definition
Spring Boot 환경 설정과 MySQL DB 생성, HikariCP를 통한 connection까지 완료했다.
이제는 도메인의 기본 요소가 될 Entity를 만들 차례이다.
이전에 ERD를 정리해 둔 것이 있으므로 이것을 JPA Entity Class로 옮기면 된다.

ERD는 위와 같고

프로젝트 파일은 도메인 별로 분류했다.
먼저 User Entity 부터 만들자.
package com.kdh.solvego.domain.user;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false,length=50,unique = true)
private String username;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(name = "created_at",nullable = false,updatable = false)
private LocalDateTime createdAt;
}
실행하면 아래와 같이 JPA 구현체인 Hibernate가 MySQL에 코드에 기재한 것처럼 user table을 만든 것을 볼 수 있다.
2026-06-18T15:02:29.225+09:00 INFO 1126 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate:
drop table if exists users
Hibernate:
create table users (
create_at datetime(6) not null,
id bigint not null auto_increment,
username varchar(50) not null,
password varchar(255) not null,
primary key (id)
) engine=InnoDB
Hibernate:
alter table users
add constraint UKr43af9ap4edm43mmtq01oddj6 unique (username)
아래는 도메인 Problem Entity이다.
package com.kdh.solvego.domain.problem;
import com.kdh.solvego.domain.user.User;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name="problems")
public class Problem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false,length=50)
private String title;
@Column(length = 100)
private String description;
@Column(nullable = false, columnDefinition = "JSON")
private String blackStones;
@Column(nullable = false, columnDefinition = "JSON")
private String whiteStones;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PlayerColor nextPlayer;
@Column(name = "answer_x", nullable = false)
private Integer answerX;
@Column(name = "answer_y", nullable = false)
private Integer answerY;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id", nullable = false)
private User creator;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
}
여기서 중요한 것은 이 creator의 Lazy Fetch 부분이다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id", nullable = false)
private User creator;
먼저, 관계형 DB에서는 creator_id와 같은 Foreign Key를 이용하여 작성자를 참조한다.
반면 JPA는 객체지향 모델링을 위하여 Foreign Key를 직접 다루는 대신 User 객체를 참조하도록 매핑한다.
즉 DB는 problem.creator_id (FK) -> users.id 라는 FK로 관계를 표현하지만,
Java 코드에는 이를 다음과 같이 User 객체를 직접 참조한다.
private User creator
이것이 ORM의 핵심 아이디어 중 하나라고 생각한다.
자. 이제 다시 creator의 LAZY Fetch를 살펴보자.
만일 EAGER를 사용한다면, Problem을 조회하는 순간 연관된 User 정보도 함께 조회하게 된다.
그러나 SolveGO의 미리 설계한 UseCase 중 사용자의 틀린 문제 조회 등 일부 경우에서는 작성자 정보가 항상 필요하지 않은 경우가 있다.
따라서 먼저 Problem만 조회하고, 실제로 작성자 정보가 필요한 시점에만 User를 조회하도록 LAZY 전략을 사용했다.
나머지 도메인, Attempt 엔티티도 정의하자.
package com.kdh.solvego.domain.attempt;
import com.kdh.solvego.domain.problem.Problem;
import com.kdh.solvego.domain.user.User;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "attempts")
public class Attempt {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "problem_id", nullable = false)
private Problem problem;
@Column(name = "selected_x", nullable = false)
private Integer selectedX;
@Column(name = "selected_y", nullable = false)
private Integer selectedY;
@Column(name = "is_correct", nullable = false)
private Boolean isCorrect;
@CreationTimestamp
@Column(name = "attempted_at", nullable = false, updatable = false)
private LocalDateTime attemptedAt;
}
Attempt 역시 User, Problem과 연관관계를 가진다.
데이터베이스에서는 각각 user_id, problem_id라는 외래 키로 저장되지만,
Java 코드에서는 User 객체와 Problem 객체를 참조하도록 표현하였다.
또한 Attempt를 조회할 때 항상 User와 Problem의 전체 정보가 필요한 것은 아니므로, 두 연관관계 모두 LAZY 전략을 사용하였다.
이렇게 모든 JPA Entity Class를 만들고 애플리케이션을 실행하면
아래와 같이 정의한대로 Hibernate가 정의한 내용에 따라 MySQL DB에 테이블을 생성하는 모습을 확인할 수 있다.
Hibernate:
alter table attempts
drop
foreign key FKi9pmoeq7wfks8vsicdhj07o40
Hibernate:
alter table attempts
drop
foreign key FK9ajal069gdogf73v6rl1ic0bc
Hibernate:
alter table problems
drop
foreign key FK2txgin34s0h1meu3xghshfs73
Hibernate:
drop table if exists attempts
Hibernate:
drop table if exists problems
Hibernate:
drop table if exists users
Hibernate:
create table attempts (
is_correct bit not null,
selected_x integer not null,
selected_y integer not null,
attempted_at datetime(6) not null,
id bigint not null auto_increment,
problem_id bigint not null,
user_id bigint not null,
primary key (id)
) engine=InnoDB
Hibernate:
create table problems (
answer_x integer not null,
answer_y integer not null,
created_at datetime(6) not null,
creator_id bigint not null,
id bigint not null auto_increment,
title varchar(50) not null,
description varchar(100),
black_stones JSON not null,
white_stones JSON not null,
next_player enum ('BLACK','WHITE') not null,
primary key (id)
) engine=InnoDB
Hibernate:
create table users (
created_at datetime(6) not null,
id bigint not null auto_increment,
username varchar(50) not null,
password varchar(255) not null,
primary key (id)
) engine=InnoDB
Hibernate:
alter table users
add constraint UKr43af9ap4edm43mmtq01oddj6 unique (username)
Hibernate:
alter table attempts
add constraint FKi9pmoeq7wfks8vsicdhj07o40
foreign key (problem_id)
references problems (id)
Hibernate:
alter table attempts
add constraint FK9ajal069gdogf73v6rl1ic0bc
foreign key (user_id)
references users (id)
Hibernate:
alter table problems
add constraint FK2txgin34s0h1meu3xghshfs73
foreign key (creator_id)
references users (id)
결론
이를 통해 프로젝트 목적에 맞게 Spring boot 설정을 완료하고, ERD로 설계했던 users, problems, attempts 테이블과 각 외래 키 관계가 JPA Entity를 기반으로 실제 MySQL DB에 생성되는 것을 확인할 수 있었다.
이제 애플리케이션이라는 건물의 기본 토대와 자재는 모두 준비되었다.
다음 글에서는 Repository, Service, Controller를 구성하여 SolveGO의 첫 번째 기능인 회원가입 API를 구현하고, 사용자의 요청이 데이터베이스까지 전달되는 전체 흐름을 살펴볼 예정이다.
'Project > SolveGO' 카테고리의 다른 글
| [SolveGO #5] 내 누군지 아니? - 로그인 인증 구현 (0) | 2026.06.22 |
|---|---|
| [SolveGO #4] 어디로 가야하오? 문 뚫기 - 회원가입 API 구현 (0) | 2026.06.20 |
| [SolveGO #2] 유죄추정의 원칙 - API 설계 (0) | 2026.06.17 |
| [SolveGO #1] 초석을 다지자 - 요구사항부터 ERD까지 (1) | 2026.06.15 |
| [SolveGO #0] 그래서 이게 먼데 - 프로젝트 아이디어 (1) | 2026.06.13 |