1. user 테이블

2. build.gradle에 Spring Data JPA, MySQL Driver 의존성 추가
이클립스에서 처음 프로젝트를 생성할 때 사용할 기능?을 선택하여 생성할 수 있다. 그러면 자동으로 build.gradle에 의존성이 추가된다. 아래는 자동으로 작성된 의존성 코드이다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
위의 4개의 줄이 각각 순서대로 Spring Data JPA, JDBC API, Spring Web, MariaDB Driver의 의존성 코드이다. 예전 Spring Boot에서는 모든 세부 설정을 프로그래머가 해야 했지만 현재는 이런 의존성 코드 한 줄만 있으면 Spring Boot가 자동으로 필요한 모든 설정을 버전까지 맞춰서 가져온다. 굉장히 편리한 기능이다!
3. application.properties에 데이터베이스 연결정보 추가
다음과 같이 데이터베이스 주소, 접속 아이디와 비밀번호 등을 작성해주면 DB 연결 준비가 완료된다.
spring.application.name=SpringBootDB
spring.datasource.hikari.jdbc-url=jdbc:mariadb://localhost:3306/test
spring.datasource.hikari.username=stdUser
spring.datasource.hikari.password=wkvmtlf2
spring.datasource.hikari.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.show-sql=true
스프링 부트는 기본적으로 HikariCP를 사용해서 DB와 연결을 맺고 관리한다. HikariCP란 JDBC 커넥션 풀 라이브러리로 빠르고 가벼우며 안정적인 내장 커넥션 풀이다. DB와 연결하는 과정은 생각보다 비싸고 느린 작업이기 때문에 연결 풀을 사용하여 빠른 속도와 안정성을 챙기는 것이다. 따라서 아래의 코드를 수정하여 HikariCP의 설정을 바꿀 수 있다.
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-timeout=30000
위는 기본 설정으로 동시에 유지할 수 있는 최대 DB 연결 개수를 10개로 설정하고, 만약 10개의 연결이 모두 사용 중일 때 새로운 요청이 들어오면 최대 30초까지 대기하다가 그 이후에도 연결을 얻지 못하면 에러를 발생시킨다는 의미이다.
4. User클래스에 @Entity와 @Id 어노테이션 붙이기
id가 Auto Increment되게 설정하였다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int age;
private User() {} // 기본 생성자 삽입
// ID 없는 생성자 (INSERT 시 사용)
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 전체 필드 생성자 (UPDATE, SELECT 시 사용)
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// Getters and Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
JPA Entity에서는 기본 생성자가 필수이다. 그 이유는 JPA가 데이터베이스에서 데이터를 가져와서 User 객체로 만들 때 리플렉션 기술을 사용하기 때문이다. JPA는 먼저 기본 생성자를 호출하여 빈 객체를 생성한 뒤, DB에서 가져온 값을 필드에 채워 넣는다. 만약 인자가 있는 다른 생성자를 만들면 컴파일러가 기본 생성자를 자동으로 만들어주지 않으므로 반드시 수동으로 기본 생성자를 작성해줘야 한다.
이 사실을 알고 나니 문득 궁금증이 들었는데, "그럼 인자가 있는 다른 생성자는 왜 쓰는거지?"였다. 정답은 기본 생성자는 JPA를 위한 것이고, 인자가 있는 다른 생성자는 내가 쓸려고 만든 것이었다. 내가 새로운 데이터를 DB에 저장하거나 수정하기 위해 객체를 생성할 때(INSERT, UPDATE), 인자가 있는 생성자를 사용하면 훨씬 편리하다.(너무 당연한 사실...😓)
추가적으로 필수 데이터를 빼먹고 객체를 생성하는 것을 막아주는 안전장치 역할도 해준다. 혹시라도 미래의 내가 필수인 요소를 안 넣고 객체를 만드는 사고를 미연에 방지해주는 것이다.
이처럼 상황에 따라 여러 생성자를 직접 생성해야 하는데, 필드가 많아지면 이것도 일일이 작성하는게 번거로울 수 있다. 이를 위해 Lombok의 @Builder 패턴이라는 게 있다고 한다. 이에 대해서는 뒤에서 자세히 다루겠다.
5. UserRepository 인터페이스 만들고 JpaRepository 상속받기
UserRepository 인터페이스를 만들고 JpaRepository를 상속받는 것만 하면 DAO 구현이 끝난다.😮
@Repository
public interface UserRepository extends JpaRepository<User, Integer>{
// 아무것도 적지 않는다.
}
이렇게 JpaRepository를 상속하는 것만으로도 구현이 끝나는 것은 부모인 JpaRepository가 필요한 메서드를 모두 정의하여 가지고 있기 때문이다. 상속을 받는 순간 save(), findById(), deleteById() 등의 메소드들이 UserRepository에 모두 복사된다.
또한 상속할 때 적은 제네릭 <User, Integer>이 스프링에게 어떤 엔티티와 ID 타입을 다룰지 알려준다. 즉 User를 통해 FROM user 쿼리를 생성하고, Integer를 통해 WHERE id = ? 조건절에 숫자를 대입하도록 알려주는 것이다.
마지막으로 스프링이 가짜 구현체인 Proxy를 몰래 만든다고 한다! 자바 문법상 인터페이스는 실행할 수 없으니 class로 구현한 객체를 만들어줘야 하는데, 여기서 나는 구현체를 만들지 않았다. 그래서 스프링 부트가 로딩 시점에 몰래 대신 만들어준다고 한다.
// (실제 파일은 없지만 메모리에 이런 걸 만듭니다)
public class UserRepository_SimpleJpaRepository_Impl implements UserRepository {
@Override
public List<User> findAll() {
// 제네릭 <User>를 보고 알아서 SQL 생성
String sql = "SELECT * FROM user";
return entityManager.createQuery(sql).getResultList();
}
@Override
public void save(User user) {
// EntityManager를 불러와서 저장 시킴
entityManager.persist(user);
}
// ... 나머지 메서드들도 자동 생성 ...
}
제미나이에게 물어보니 이런 클래스를 동적으로 생성해준다고 한다. 그리고 스프링 부트가 만들어준 구현체(Bean)를 내가 Autowired할 때 주입해 준다. 이 기술을 동적 프록시(Dynamic Proxy)라고 한다.(너무 신기해!🤓)
6. UserService와 DBController 구현
UserService에서 UserRepository를 주입받은 후 CRUD 로직을 구현한다.
@Service // 이 클래스가 Spring의 서비스 컴포넌트임을 나타냄 (비즈니스 로직 담당)
public class UserService {
// UserRepository를 주입받기 위한 필드 (데이터베이스 접근을 위한 인터페이스)
private final UserRepository repository;
// 생성자를 통한 의존성 주입 (Spring이 자동으로 주입해줌)
public UserService(UserRepository repository) {
this.repository = repository;
}
// CREATE: 새로운 사용자 생성 및 저장
public User create(User user) {
return repository.save(user); // JPA의 save() 메서드를 통해 DB에 저장
}
// READ: 모든 사용자 목록 조회
public List<User> read() {
return repository.findAll(); // 모든 사용자 엔티티를 리스트로 반환
}
// READ: ID로 사용자 조회 (없으면 null 반환)
public User readById(int id) {
return repository.findById(id).orElse(null); // Optional에서 값이 없으면 null 반환
}
// UPDATE: ID로 사용자 조회 후 이름과 나이를 수정
public User update(int id, User updatedUser) {
User user = readById(id); // 기존 사용자 조회
if (user == null) {
return null; // 사용자가 없으면 null 반환
}
// 사용자 정보 업데이트
user.setName(updatedUser.getName());
user.setAge(updatedUser.getAge());
return repository.save(user); // 수정된 사용자 저장
}
// DELETE: ID로 사용자 삭제 (성공 여부를 Boolean으로 반환)
public Boolean delete(int id) {
if (!repository.existsById(id)) {
return false; // 사용자가 없으면 false 반환
}
repository.deleteById(id); // 사용자 삭제
return true; // 삭제 성공 시 true 반환
}
}
DBController에서 UserService를 호출한 후 CRUD가 DB와 연동되도록한다.
@Controller // 이 클래스가 Spring의 컨트롤러임을 나타냄
@RequestMapping("/user") // 모든 요청 경로 앞에 "/user"를 붙임
public class SpringDBController {
// 사용자 관련 비즈니스 로직을 처리하는 서비스 객체
private final UserService service;
// 생성자를 통해 UserService를 주입받음
public SpringDBController(UserService service) {
this.service = service;
}
// CREATE: 사용자 등록 처리
@PostMapping("/submitForm") // POST 요청 "/user/submitForm" 처리
@ResponseBody // 반환값을 HTTP 응답 본문으로 직접 출력
public String create(@RequestParam("name") String name, @RequestParam("age") int age) {
service.create(new User(name, age)); // 사용자 생성 및 저장
return "<h3>등록 완료</h3><a href='/user-crud.html'>돌아가기</a>"; // 결과 HTML 반환
}
// READ: ID로 사용자 조회
@GetMapping // GET 요청 "/user?id=..." 처리
@ResponseBody
public String readById(@RequestParam("id") int id) {
User user = service.readById(id); // ID로 사용자 조회
if (user == null) return "사용자 없음!"; // 없으면 메시지 반환
// 사용자 정보 HTML로 반환
return "<h3>사용자 정보</h3>" +
"<p>ID: " + user.getId() + "</p>" +
"<p>이름: " + user.getName() + "</p>" +
"<p>나이: " + user.getAge() + "</p>" +
"<a href='/user-crud.html'>돌아가기</a>";
}
// READ: 전체 사용자 목록 조회
@GetMapping("/list") // GET 요청 "/user/list" 처리
@ResponseBody
public String list() {
List<User> users = service.read(); // 모든 사용자 조회
// HTML 테이블로 사용자 목록 구성
StringBuilder html = new StringBuilder("<h3>전체 사용자 목록</h3><table border='1'><tr><th>ID</th><th>이름</th><th>나이</th></tr>");
for (User u : users) {
html.append("<tr><td>").append(u.getId()).append("</td><td>")
.append(u.getName()).append("</td><td>")
.append(u.getAge()).append("</td></tr>");
}
html.append("</table><br><a href='/user-crud.html'>돌아가기</a>");
return html.toString(); // HTML 반환
}
// UPDATE: 사용자 정보 수정
@PostMapping("/updateForm") // POST 요청 "/user/updateForm" 처리
@ResponseBody
public String update(@RequestParam("id") int id,
@RequestParam("name") String name,
@RequestParam("age") int age) {
User result = service.update(id, new User(name, age)); // 사용자 수정
if (result == null) {
return "<h3>수정 실패: 사용자를 찾을 수 없습니다.</h3><a href='/user-crud.html'>돌아가기</a>"; // 실패 시 메시지
}
return "<h3>수정 완료</h3><a href='/user-crud.html'>돌아가기</a>"; // 성공 시 메시지
}
// DELETE: 사용자 삭제
@PostMapping("/deleteForm") // POST 요청 "/user/deleteForm" 처리
@ResponseBody
public String delete(@RequestParam("id") int id) {
boolean success = service.delete(id); // 사용자 삭제
if (!success) {
return "<h3>삭제 실패: 사용자를 찾을 수 없습니다.</h3><a href='/user-crud.html'>돌아가기</a>"; // 실패 시 메시지
}
return "<h3>삭제 완료</h3><a href='/user/list'>전체 목록 보기</a>"; // 성공 시 메시지
}
}
7. DBConfiguration 생성
@Configuration
@PropertySource("classpath:/application.properties")
public class DBConfiguration {
@Bean
@ConfigurationProperties(prefix="spring.datasource.hikari")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
public DataSource dataSource() throws Exception{
System.out.println("시작!");
DataSource dataSource = new HikariDataSource(hikariConfig());
return dataSource;
}
}
8. input.html 입력폼 생성 + user-crud.html 작성
<!-- input.html -->
<html>
<body>
<meta charset="UTF-8">
<form method="post" action="http://127.0.0.1:8080/user/submitForm" accept-charset="UTF-8">
<input type="text" name="name" value="이름" />
<input type="number" name="age" value="5" />
<button type="submit">보내기</button>
</form>
</body>
</html>
<!-- user-crud.html -->
<!DOCTYPE html>
<html lang="ko"> <!-- 문서의 언어를 한국어로 설정 -->
<head>
<meta charset="UTF-8"> <!-- 문자 인코딩을 UTF-8로 설정 -->
<title>사용자 CRUD - action 방식</title> <!-- 브라우저 탭에 표시될 제목 -->
</head>
<body>
<h1>사용자 CRUD (action 방식)</h1> <!-- 페이지의 메인 제목 -->
<!-- CREATE: 사용자 등록 폼 -->
<h2>사용자 등록</h2>
<form method="post" action="/user/submitForm"> <!-- POST 방식으로 /user/submitForm에 데이터 전송 -->
이름: <input type="text" name="name" required /> <!-- 이름 입력 필드 (필수) -->
나이: <input type="number" name="age" required /> <!-- 나이 입력 필드 (필수) -->
<button type="submit">등록</button> <!-- 폼 제출 버튼 -->
</form>
<!-- READ (단일 조회): ID로 사용자 조회 -->
<h2>사용자 조회 (ID로)</h2>
<form method="get" action="/user"> <!-- GET 방식으로 /user에 ID 전송 -->
ID: <input type="number" name="id" required /> <!-- 조회할 사용자 ID 입력 -->
<button type="submit">조회</button> <!-- 조회 버튼 -->
</form>
<!-- READ (전체 조회): 모든 사용자 목록 조회 -->
<h2>전체 사용자 조회</h2>
<form method="get" action="/user/list"> <!-- GET 방식으로 /user/list 요청 -->
<button type="submit">전체 목록 보기</button> <!-- 전체 목록 보기 버튼 -->
</form>
<!-- UPDATE: 사용자 정보 수정 -->
<h2>사용자 수정</h2>
<form method="post" action="/user/updateForm"> <!-- POST 방식으로 /user/updateForm에 데이터 전송 -->
ID: <input type="number" name="id" required /> <!-- 수정할 사용자 ID 입력 -->
새 이름: <input type="text" name="name" required /> <!-- 새 이름 입력 -->
새 나이: <input type="number" name="age" required /> <!-- 새 나이 입력 -->
<button type="submit">수정</button> <!-- 수정 버튼 -->
</form>
<!-- DELETE: 사용자 삭제 -->
<h2>사용자 삭제</h2>
<form method="post" action="/user/deleteForm"> <!-- POST 방식으로 /user/deleteForm에 ID 전송 -->
ID: <input type="number" name="id" required /> <!-- 삭제할 사용자 ID 입력 -->
<button type="submit">삭제</button> <!-- 삭제 버튼 -->
</form>
</body>
</html>
9. SpringBootDbApplication 실행
@SpringBootApplication
public class SpringBootDbApplication implements CommandLineRunner {
@Autowired
private UserService userService;
public static void main(String[] args) {
System.out.println("Project : Spring Boot DB start!");
SpringApplication.run(SpringBootDbApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println("Spring DB is running");
List <User >users=userService.read();
for (User u : users) {
System.out.println(u.getId() + u.getName());
}
}
}
10. 실행 결과
처음 실행 시 콘솔

데이터 삽입하기

데이터 추가 후 db

'돌아가기' 누른 후 화면
아래 화면에서 등록, 조회, 수정, 삭제가 모두 가능하다.

예시로 ID 1번을 조회하면 결과를 출력해준다.

@Getter, @Setter, @Builder, @ToString, @NoArgsConstructor 등을 통해 자동으로 코드 생성compileOnly'org.projectlombok:lombok:1.18.26‘
annotationProcessor'org.projectlombok:lombok:1.18.26'
롬복을 사용하면 위에서 작성한 코드가 다음과 같이 매우 간략해진다. 위에서 언급한 @Builder 어노테이션도 추가해보았다.
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int age;
}
빌더 패턴을 사용하지 않는다면 User user = new User("철수", 20); 와 같이 순서를 맞춰 생성자를 호출해야 한다. 하지만 롬복의 @Builder를 적용하면 아래 코드와 같이 훨씬 편리하고 직관적으로 객체를 생성할 수 있다.
User user = User.builder()
.name("철수")
.age(20)
.build();
롬복은 내부적으로 빌더 클래스를 통해 객체를 조립해준다. 따라서 인자의 순서를 신경 쓸 필요도 없고, 필요한 데이터만 골라서 넣을 수 있어 가독성이 크게 좋아진다.
다만 주의할 점은 @Builder는 객체를 생성할 때 모든 필드를 초기화하는 생성자를 사용하기 때문에 @AllArgsConstructor가 함께 있어야 동작한다.
Spring Data JPA는 내부적으로 JPA -> Hibernate -> JDBC 순으로 동작하며, JDBC는 가장 기초적인 DB 접근 기술이다. 또한 자동 쿼리 처리 기능이 있는데, CrudRepository나 JpaRepository 덕분에 복잡한 SQL 없이도 기본 CRUD를 손쉽게 구현할 수 있다. 그리고 객체지향 방식으로 테이블을 다룰 수 있어 코드의 가독성과 유지보수에 용이한 엔티티 중심 설계이다.
| 항목 | JDBC | Spring Data JPA |
|---|---|---|
| 접근 방식 | 저수준 API (직접 SQL 작성) | 고수준 ORM + 자동화 |
| 쿼리 작성 | SELECT, INSERT 등 SQL 직접 작성 | 메서드 이름 기반 자동 생성 또는 JPQL |
| 코드 양 | 많음 (Connection, Statement, ResultSet 등 수동 처리) | 적음 (인터페이스만 작성하면 CRUD 자동 생성) |
| 예외처리 | 직접 try-catch | Spring의 예외 추상화로 간편 처리 |
| 유지보수 | 어렵고 반복적 | 구조화되어 있고 수정/확장 용이 |
| 구현 난이도 | 낮지만 구현 부담 큼 | 상대적으로 쉬움 |
| 성능 제어 | SQL 직접 튜닝 가능 | Hibernate 기반으로 튜닝 가능(복잡한 쿼리는 nativeQuery 사용) |
JDBC를 쓸 때는 SQL문도 내가 쓰고, 상태 관리도 내가 다 해줘야 하고, 프로젝트 구조도 내가 알아서 예쁘게 나눠서 정리해야 했다. 그런데 Spring Boot를 사용하니 체계적인 패키지 구조를 나누기도 편하고 세세하게 관리해줘야 했던 설정들이 모두 어노테이션으로 정리되고, 내가 따로 db 연결해야 하는 것도 없이 의존성만 추가하면 되니 할 맛이 난다.😋 배우면 배울수록 편하고 매력적인 툴인 것 같다!