Docker

Docker로 스프링부트 JAR을 컨테이너화 해보자 [H2DB]

rexondex 2024. 11. 13. 15:48

자바 스프링부트와 AWS 클라우드 배포를 생각하던 중 도커와 쿠버네티스의 역할이 궁금해졌습니다.

 

그래서 이번엔 도커에 대한 궁금증을 풀기 위해 스프링부트 jar 파일을 도커 컨테이너화하고, 컨테이너를 실행하여 동작까지 확인하는 것이 목표입니다.

 

 

우선, Docker를 설치해야 합니다. [ https://www.docker.com/products/docker-desktop/ ]

도커 공식사이트에서 다운로드 받을 수 있습니다.

 

도커 설치 후, 아래 가이드로 진행합니다.


스프링부트 3.3.5 / JDK 17 사용

 

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 파일을 위치시킵니다.

gradle의 bootJar 실행
bootJar를 실행하면 build > libs 에 스냅샷 jar이 생성됨
target 폴더 안으로 옮겨둡니다


- 루트 경로에서 터미널을 열어 도커 이미지를 빌드합니다.

// 이 명령은 현재 디렉토리에 있는 Dockerfile을 사용하여 userservice라는 이름의 이미지를 빌드합니다.
docker build -t userservice .

이미지 빌드

 

- 빌드된 이미지를 사용하여 Docker 컨테이너를 실행합니다. 

docker run -p 8080:8080 userservice

스프링부트 Run

 


- localhost:8080 접속해서 index.html 동작이 잘 되는지 확인해봅니다.

 

h2 메모리에 잠깐 저장되었다가 사라지는 데이터입니다.

h2 메모리 대신 oracle, mysql 등 다른 데이터베이스에도 연결해볼 수 있겠습니다.

 

도커로 실행한 localhost:8080 index.html

 


:: 후기 ::

스프링부트 애플리케이션을 Docker 이미지로 빌드하고, 터미널에서 실행하여 접속 및 동작을 확인했습니다. 이번 예제에서는 H2 메모리를 사용했지만, 필요한 경우 application.properties 파일을 수정해 Oracle이나 MySQL 등 외부 데이터베이스에 연결할 수도 있습니다.

 

자바는 JVM(자바 가상 머신)만 있으면 JAR 파일을 실행할 수 있기 때문에 OS에 상관없이 자바 코드를 실행할 수 있습니다. 그렇다면, 왜 굳이 Docker를 사용해 컨테이너화했을까요? Docker를 사용하는 이유는 무엇일까요?

 

[ Docker의 언어 및 런타임 환경 독립성 ]

 

Docker는 애플리케이션을 컨테이너 단위로 실행하며, 각 컨테이너는 애플리케이션이 필요로 하는 모든 런타임과 의존성을 포함하고 있습니다. 이로 인해 Docker는 다음과 같은 이점을 제공합니다:

  1. 언어 독립성: 같은 서버에서 자바 애플리케이션, 파이썬 애플리케이션, 노드 애플리케이션을 각각 컨테이너로 실행할 수 있습니다. 각 컨테이너는 자신에게 필요한 언어와 라이브러리만을 포함하고 있으므로, 애플리케이션이 특정 언어나 라이브러리에 의존하더라도 서로 충돌하지 않고 독립적으로 작동할 수 있습니다.
  2. 환경 일관성 유지: 개발 환경, 테스트 환경, 배포 환경에서 동일한 Docker 이미지를 사용할 수 있어, 모든 환경에서 동일하게 동작하는 애플리케이션을 쉽게 만들 수 있습니다. 예를 들어, "내 로컬에서는 잘 돌아가는데 서버에서는 오류가 난다"와 같은 문제를 줄일 수 있습니다.
  3. 호환성과 충돌 방지: Docker 컨테이너는 호스트 OS와 분리되어 독립적으로 실행됩니다. 각 애플리케이션은 컨테이너 안에서 필요한 런타임과 의존성을 갖추고 있으므로, 버전 충돌 문제나 환경 설정 충돌을 피할 수 있습니다.

 

이처럼 Docker는 자바뿐만 아니라 다른 언어로 작성된 마이크로서비스도 컨테이너로 묶어 일관된 환경에서 실행할 수 있도록 합니다.

Docker로 이미지를 빌드하고 실행해 보면서, Docker의 핵심 가치를 이해하고 사용해야 하는 이유를 다시 한번 생각하게 되었습니다.