자바 스프링부트와 AWS 클라우드 배포를 생각하던 중 도커와 쿠버네티스의 역할이 궁금해졌습니다.
그래서 이번엔 도커에 대한 궁금증을 풀기 위해 스프링부트 jar 파일을 도커 컨테이너화하고, 컨테이너를 실행하여 동작까지 확인하는 것이 목표입니다.
우선, Docker를 설치해야 합니다. [ https://www.docker.com/products/docker-desktop/ ]
도커 공식사이트에서 다운로드 받을 수 있습니다.
도커 설치 후, 아래 가이드로 진행합니다.
0. application.properties에 코드를 추가합니다.
# H2 memory
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# JPA config
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
# H2 console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
그리고 build.gradle에 코드를 추가합니다.
bootJar {
mainClass = 'com.example.userservice.UserserviceApplication' // 여기에 main class를 명시
}
1. UserRepository 작성
package com.example.userservice.repository;
import com.example.userservice.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
2. User 엔터티 작성
package com.example.userservice.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "users") // H2 테이블 이름을 "users"로 지정
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// 기본 생성자
public User() {}
// 생성자
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getter와 Setter 추가
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
3. UserService 작성
package com.example.userservice.service;
import com.example.userservice.entity.User;
import com.example.userservice.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public User createUser(User user) {
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
4. UserController 작성
package com.example.userservice.controller;
import com.example.userservice.entity.User;
import com.example.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
5. templates 패키지의 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Management</title>
<style>
body { font-family: Arial, sans-serif; }
h2 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
table, th, td { border: 1px solid #ddd; padding: 8px; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h2>User Management</h2>
<!-- User Form -->
<form id="userForm">
<label for="name">Name:</label>
<input type="text" id="name" required>
<label for="email">Email:</label>
<input type="email" id="email" required>
<button type="submit">Add User</button>
</form>
<!-- User Table -->
<h3>User List</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="userTableBody"></tbody>
</table>
<script>
const API_URL = 'http://localhost:8080/users'; // API URL
// Fetch and display all users
async function fetchUsers() {
const response = await fetch(API_URL);
const users = await response.json();
const userTableBody = document.getElementById('userTableBody');
userTableBody.innerHTML = ''; // Clear table
users.forEach(user => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
<td>
<button onclick="deleteUser(${user.id})">Delete</button>
</td>
`;
userTableBody.appendChild(row);
});
}
// Add new user
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email })
});
if (response.ok) {
document.getElementById('name').value = '';
document.getElementById('email').value = '';
fetchUsers();
} else {
alert('Failed to add user.');
}
});
// Delete user
async function deleteUser(id) {
const response = await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
if (response.ok) {
fetchUsers();
} else {
alert('Failed to delete user.');
}
}
// Initialize user list
fetchUsers();
</script>
</body>
</html>
이 html은 localhost:8080으로 접속했을 때 보여줄 index 화면이며, 기본적인 Create, Read, Delete 작업을 수행합니다.
6. 루트 경로에 파일명 Dockerfile 작성합니다 (확장자 없음)
루트 경로는 build.gradle이 있는 경로와 같은 경로입니다.
# Step 1: Base image with Java
FROM openjdk:17-jdk-slim AS build
# Step 2: Set working directory
WORKDIR /app
# Step 3: Copy the build artifact (JAR file)
COPY target/userservice-0.0.1-SNAPSHOT.jar /app/userservice.jar
# Step 4: Expose the port your app will run on
EXPOSE 8080
# Step 5: Run the application
ENTRYPOINT ["java", "-jar", "userservice.jar"]
build.gradle이 있는 루트 경로에서 target 폴더를 생성합니다.
target 폴더 안에는 스프링부트 gradle의 bootJar로 생성된 스냅샷 jar 파일을 위치시킵니다.
- 루트 경로에서 터미널을 열어 도커 이미지를 빌드합니다.
// 이 명령은 현재 디렉토리에 있는 Dockerfile을 사용하여 userservice라는 이름의 이미지를 빌드합니다.
docker build -t userservice .
- 빌드된 이미지를 사용하여 Docker 컨테이너를 실행합니다.
docker run -p 8080:8080 userservice
- localhost:8080 접속해서 index.html 동작이 잘 되는지 확인해봅니다.
h2 메모리에 잠깐 저장되었다가 사라지는 데이터입니다.
h2 메모리 대신 oracle, mysql 등 다른 데이터베이스에도 연결해볼 수 있겠습니다.
:: 후기 ::
스프링부트 애플리케이션을 Docker 이미지로 빌드하고, 터미널에서 실행하여 접속 및 동작을 확인했습니다. 이번 예제에서는 H2 메모리를 사용했지만, 필요한 경우 application.properties 파일을 수정해 Oracle이나 MySQL 등 외부 데이터베이스에 연결할 수도 있습니다.
자바는 JVM(자바 가상 머신)만 있으면 JAR 파일을 실행할 수 있기 때문에 OS에 상관없이 자바 코드를 실행할 수 있습니다. 그렇다면, 왜 굳이 Docker를 사용해 컨테이너화했을까요? Docker를 사용하는 이유는 무엇일까요?
[ Docker의 언어 및 런타임 환경 독립성 ]
Docker는 애플리케이션을 컨테이너 단위로 실행하며, 각 컨테이너는 애플리케이션이 필요로 하는 모든 런타임과 의존성을 포함하고 있습니다. 이로 인해 Docker는 다음과 같은 이점을 제공합니다:
- 언어 독립성: 같은 서버에서 자바 애플리케이션, 파이썬 애플리케이션, 노드 애플리케이션을 각각 컨테이너로 실행할 수 있습니다. 각 컨테이너는 자신에게 필요한 언어와 라이브러리만을 포함하고 있으므로, 애플리케이션이 특정 언어나 라이브러리에 의존하더라도 서로 충돌하지 않고 독립적으로 작동할 수 있습니다.
- 환경 일관성 유지: 개발 환경, 테스트 환경, 배포 환경에서 동일한 Docker 이미지를 사용할 수 있어, 모든 환경에서 동일하게 동작하는 애플리케이션을 쉽게 만들 수 있습니다. 예를 들어, "내 로컬에서는 잘 돌아가는데 서버에서는 오류가 난다"와 같은 문제를 줄일 수 있습니다.
- 호환성과 충돌 방지: Docker 컨테이너는 호스트 OS와 분리되어 독립적으로 실행됩니다. 각 애플리케이션은 컨테이너 안에서 필요한 런타임과 의존성을 갖추고 있으므로, 버전 충돌 문제나 환경 설정 충돌을 피할 수 있습니다.
이처럼 Docker는 자바뿐만 아니라 다른 언어로 작성된 마이크로서비스도 컨테이너로 묶어 일관된 환경에서 실행할 수 있도록 합니다.
Docker로 이미지를 빌드하고 실행해 보면서, Docker의 핵심 가치를 이해하고 사용해야 하는 이유를 다시 한번 생각하게 되었습니다.
'Docker' 카테고리의 다른 글
docker-compose로 여러 컨테이너를 동시에 실행해보자 [Docker] (0) | 2024.11.14 |
---|---|
Node 마이크로서비스를 Docker 컨테이너화 해보자 [REST] (0) | 2024.11.14 |