14. 실무 확장: 테스트, 빌드, 모듈, JDBC, 그리고 Spring 입문
지금까지는 자바의 문법, 객체지향, 컬렉션, 예외, 스트림, JVM 같은 핵심 뼈대를 차근차근 쌓아 왔다. 이제 마지막으로 필요한 것은 학습용 코드가 실무형 프로젝트로 자라는 순간을 이해하는 일이다. 콘솔에서 잘 돌아가던 프로그램이 실제 협업과 배포의 세계로 들어가려면, 단순히 main()만 잘 쓰는 것으로는 부족하다.
실무에서는 늘 이런 질문이 따라온다. "이 코드가 정말 맞나?", "다른 개발자 컴퓨터에서도 같은 방식으로 빌드되나?", "코드가 커져도 구조가 버티나?", "데이터베이스와 안전하게 연결되나?", "Spring으로 옮기면 무엇이 편해지나?"
이 장은 그 질문들에 답하는 장이다. 말하자면, 자바 학습용 자전거에 드디어 변속기와 브레이크, 그리고 번호판을 달아 주는 시간이다.

테스트는 귀찮은 검사가 아니라 미래의 보험이다
처음 프로그램을 배울 때는 실행 결과를 눈으로 확인하는 것만으로도 꽤 만족스럽다. 하지만 기능이 늘어나면 눈으로 확인하는 방식은 금방 한계를 드러낸다. 어제 고친 코드가 오늘 다른 기능을 깨뜨릴 수도 있고, 한 사람이 아니라 여러 사람이 함께 개발하면 "내 컴퓨터에서는 되는데요?"라는 전설의 문장이 등장한다.
이때 필요한 것이 자동화된 테스트다. 자바에서 가장 대표적인 테스트 도구는 JUnit이며, 현대 자바 프로젝트에서는 보통 JUnit 5 계열인 Jupiter를 사용한다. Gradle에서 JUnit 5를 사용하려면 useJUnitPlatform() 설정이 필요하고, JUnit 5는 테스트 API와 실행 엔진을 분리해서 구성하는 방식이 기본이다.1
테스트는 크게 두 종류로 생각하면 이해가 쉽다.
단위 테스트: 메서드나 클래스 하나를 작게 검증한다.
통합 테스트: 데이터베이스, 파일, 외부 시스템과 함께 동작하는 흐름을 확인한다.
회귀 테스트: 예전에 되던 기능이 여전히 되는지 다시 확인한다.
가장 먼저 익혀야 할 것은 단위 테스트다. 예를 들어 할인 금액을 계산하는 클래스가 있다고 해 보자.
public class DiscountService {
public int applyDiscount(int price, int percent) {
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("할인율은 0~100 사이여야 합니다.");
}
return price - (price * percent / 100);
}
}이 클래스를 테스트하면 다음과 같다.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void 정상적인_할인율이_주어지면_할인가를_반환한다() {
assertEquals(9000, service.applyDiscount(10000, 10));
}
@Test
void 잘못된_할인율이_주어지면_예외가_발생한다() {
assertThrows(IllegalArgumentException.class,
() -> service.applyDiscount(10000, 120));
}
}이 테스트의 좋은 점은 아주 단순하다. 코드를 고친 뒤 다시 실행하면, 예전 규칙이 여전히 지켜지는지 즉시 알 수 있다. 사람의 기억보다 테스트가 더 정확하고, 커피를 마시러 가도 테스트는 불평하지 않는다.
테스트를 작성할 때는 몇 가지 습관이 중요하다.
테스트 이름만 봐도 무엇을 검증하는지 알 수 있게 쓴다.
테스트끼리 서로 의존하지 않게 만든다.
외부 API나 실제 데이터베이스가 꼭 필요하지 않다면 가능한 한 작고 빠르게 만든다.
실무에 가까워지면 Mockito 같은 목(mock) 도구도 자주 쓴다. 목은 진짜 객체 대신 사용하는 가짜 객체다. 예를 들어 결제 서버나 메일 서버처럼 실제로 호출하면 느리거나 위험한 대상을 테스트용 가짜로 바꿔서 검증할 수 있다. 핵심은 이것이다. 좋은 테스트는 기능을 검증하면서도, 실행이 빠르고, 반복 가능해야 한다.
빌드 도구는 프로젝트의 조립 설명서다
테스트가 코드의 안전벨트라면, 빌드 도구는 프로젝트의 조립 설명서다. 실무 프로젝트에서는 소스 코드를 컴파일하고, 필요한 라이브러리를 내려받고, 테스트를 실행하고, 실행 파일이나 배포 산출물을 만드는 과정이 반복된다. 이 과정을 손으로 하나하나 처리하면 금방 지친다. 그래서 Maven이나 Gradle 같은 빌드 도구를 사용한다.
둘 다 잘 쓰이는 도구지만 느낌은 조금 다르다.
| 구분 | Maven | Gradle |
|---|---|---|
| 설정 방식 | XML 중심 | Groovy/Kotlin DSL 중심 |
| 느낌 | 규칙이 분명하고 정형적 | 유연하고 확장성이 큼 |
| 초보자 인상 | 구조가 보이기 쉬움 | 익숙해지면 편하지만 처음엔 낯설 수 있음 |
| 자주 보는 파일 | pom.xml | build.gradle, build.gradle.kts |
Maven의 장점은 프로젝트 구조가 비교적 정형화되어 있다는 점이다. 팀에서 규칙을 맞추기 쉽고, 문서를 찾아도 비슷한 형태가 많이 나온다. 반면 Gradle은 설정이 더 유연하고, 대형 프로젝트에서 빠른 빌드와 확장성 측면에서 선호되기도 한다.
JUnit 테스트를 Maven으로 구성하면 대체로 이런 모습이 된다.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>최신안정버전</version>
<scope>test</scope>
</dependency>
</dependencies>Gradle은 더 짧게 적는 경우가 많다.
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:최신안정버전'
}
tasks.named('test') {
useJUnitPlatform()
}처음에는 Maven과 Gradle이 단순히 "라이브러리 다운로드 도구"처럼 보일 수 있다. 하지만 실제로는 훨씬 많은 일을 한다.
컴파일
테스트 실행
패키징(JAR 생성)
의존성 관리
플러그인 기반 확장
실무에서 자주 쓰는 명령도 알아 두면 좋다.
mvn test
mvn package
./gradlew test
./gradlew build여기서 특히 눈여겨볼 것은 gradlew다. 이것은 Gradle Wrapper로, 팀원마다 Gradle을 따로 설치하지 않아도 프로젝트가 요구하는 버전으로 빌드할 수 있게 도와준다. 협업에서 생각보다 큰 차이를 만드는 부분이다. "버전이 달라서 안 돼요"라는 말을 줄여 준다.
빌드 도구를 도입하면 프로젝트가 비로소 혼자만의 파일 묶음이 아니라, 누구나 같은 절차로 재현 가능한 제품에 가까워진다.
모듈은 패키지보다 한 단계 더 큰 경계선이다
자바를 배우면서 package는 이미 익숙해졌을 것이다. 패키지는 관련 클래스들을 묶는 단위다. 그런데 프로젝트가 커지면 패키지만으로는 부족할 때가 있다. 이때 등장하는 개념이 모듈이다.
자바에서 모듈이라고 할 때는 보통 JPMS(Java Platform Module System) 를 뜻한다. 쉽게 말해, 어떤 패키지를 외부에 공개하고 어떤 패키지는 내부 구현으로 숨길지 더 강하게 선언하는 구조다.
예를 들어 회원 기능을 별도 모듈로 만든다고 해 보자.
module com.example.member {
exports com.example.member.api;
}그리고 이 모듈을 사용하는 애플리케이션은 다음처럼 의존성을 선언한다.
module com.example.app {
requires com.example.member;
}이 구조의 장점은 분명하다. 프로젝트가 커져도 "어디까지가 공개 API인지", "어떤 모듈이 어떤 모듈에 의존하는지"가 더 또렷해진다. 패키지는 서랍 정리라면, 모듈은 방과 방 사이에 문을 달고 출입 권한을 정하는 느낌에 가깝다.
다만 초보자가 가장 헷갈리는 지점이 하나 있다. 자바 모듈(JPMS) 과 빌드 도구의 멀티 모듈 프로젝트는 비슷해 보이지만 다른 개념이다.
자바 모듈:
module-info.java로 공개 범위와 의존 관계를 명시한다.멀티 모듈 빌드:
common,api,app같은 하위 프로젝트를 나누어 빌드한다.둘은 함께 쓸 수도 있지만, 반드시 같이 써야 하는 것은 아니다.
학습 단계에서는 모듈을 무리해서 깊게 파고들 필요는 없다. 다만 코드가 커질수록 경계를 명확히 세우는 사고방식이 중요하다는 점은 꼭 기억해 두자. 실무에서는 바로 그 경계선이 유지보수 비용을 크게 좌우한다.
JDBC는 자바와 데이터베이스가 대화하는 표준 언어다
이제 프로젝트가 진짜 서비스처럼 보이려면 데이터를 다뤄야 한다. 메모리에만 저장되는 정보는 프로그램이 종료되면 사라진다. 사용자의 회원 정보, 주문 내역, 게시글, 댓글 같은 데이터는 보통 관계형 데이터베이스에 저장한다. 자바에서 데이터베이스와 연결하는 기본 표준이 바로 JDBC(Java Database Connectivity) 다. JDBC는 자바 프로그램이 데이터 소스에 연결하고, SQL을 보내고, 결과를 받아 처리할 수 있게 해 주는 표준 API다.2
JDBC의 기본 흐름은 매우 중요하다.
Connection: 데이터베이스와 연결한다.PreparedStatement: SQL과 파라미터를 준비한다.ResultSet: 조회 결과를 한 줄씩 읽는다.
직접 JDBC를 쓰는 코드는 대략 이런 모양이다.
import java.sql.*;
public class BookDao {
public void findBooks() {
String url = "jdbc:mysql://localhost:3306/bookdb";
String user = "bookuser";
String password = "secret";
String sql = "select id, title, price from books where price >= ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, 10000);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String title = rs.getString("title");
int price = rs.getInt("price");
System.out.println(id + " / " + title + " / " + price);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}여기서 중요한 포인트는 세 가지다.
첫째, SQL은 문자열이지만 아무 문자열이나 던지면 안 된다. 문법이 맞아야 하고, 특히 사용자 입력을 직접 이어 붙이지 않도록 조심해야 한다. 그래서 Statement보다 PreparedStatement를 더 자주 쓴다. 파라미터를 따로 바인딩하면 보안과 가독성 면에서 훨씬 안전하다.
둘째, JDBC는 리소스 정리가 정말 중요하다. 연결을 열었으면 닫아야 한다. ResultSet, PreparedStatement, Connection을 제대로 닫지 않으면 연결이 누수되어 나중에 큰 문제를 일으킨다. 그래서 try-with-resources 문법을 적극적으로 쓰는 습관이 좋다.
셋째, 실무에서는 DriverManager보다 DataSource를 더 자주 사용한다. JDBC 문서에서도 가능하면 DataSource를 사용하는 방식이 권장된다.2 이유는 설정 관리, 커넥션 풀(connection pool), 프레임워크 연동이 더 편하기 때문이다.
트랜잭션도 빠뜨릴 수 없다. 은행 송금처럼 여러 SQL이 한 덩어리로 모두 성공하거나 모두 실패해야 하는 상황이 있다. 이럴 때는 자동 커밋을 끄고 commit() 또는 rollback()을 직접 제어한다.
conn.setAutoCommit(false);
try {
// 여러 SQL 실행
conn.commit();
} catch (Exception e) {
conn.rollback();
}JDBC는 다소 투박해 보일 수 있다. 하지만 바로 그 투박함 덕분에 데이터 접근의 본질이 잘 보인다. 뒤에서 배울 MyBatis, JPA, Spring Data 같은 기술도 결국은 이 흐름 위에 서 있다.
Spring은 JDBC의 반복 작업을 크게 줄여 준다
순수 JDBC는 공부할 때 매우 좋지만, 실무에서 계속 직접 다루다 보면 반복 코드가 꽤 많다. 연결 열기, 닫기, 예외 처리, ResultSet 순회, 객체 매핑 같은 작업이 계속 나타난다. Spring JDBC의 핵심 도구인 JdbcTemplate 은 이런 지루한 반복 작업을 줄여 준다. Spring 문서에 따르면 JdbcTemplate은 연결 생성과 해제, statement 실행, ResultSet 처리, JDBC 예외를 더 일관된 예외 계층으로 바꾸는 일까지 도와준다.3
Spring Boot에서는 보통 spring-boot-starter-jdbc 의존성과 application.properties 설정을 통해 DataSource를 구성하고, 필요하면 H2 같은 내장 데이터베이스를 개발이나 테스트에 활용할 수 있다.4
설정은 보통 이런 모습이다.
spring.datasource.url=jdbc:mysql://localhost:3306/bookdb
spring.datasource.username=bookuser
spring.datasource.password=secret그리고 저장소 클래스는 훨씬 단정해진다.
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class BookRepository {
private final JdbcTemplate jdbcTemplate;
public BookRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Book> findAll() {
String sql = "select id, title, price from books";
return jdbcTemplate.query(sql, (rs, rowNum) ->
new Book(
rs.getLong("id"),
rs.getString("title"),
rs.getInt("price")
)
);
}
}이 코드가 깔끔해 보이는 이유는 개발자가 진짜로 신경 써야 할 것만 남았기 때문이다. SQL이 무엇인지, 결과를 어떤 객체로 바꿀지만 적으면 된다. 나머지 연결 관리와 예외 처리는 프레임워크가 많이 덜어 준다.
Spring을 처음 만나는 사람은 종종 "그래서 Spring이 정확히 뭘 해 주는 거죠?"라고 묻는다. 아주 단순하게 말하면 이렇다.
객체 생성과 연결을 관리해 준다.
반복적인 인프라 코드를 줄여 준다.
웹, 데이터베이스, 테스트, 보안 같은 실무 기능을 잘 묶어 준다.
즉, Spring은 마법이라기보다 불필요한 반복을 걷어 내고 구조를 정리해 주는 도구 상자에 가깝다.
JDBC에서 Spring으로 넘어갈 때 무엇이 달라질까
둘의 차이를 감각적으로 정리하면 이해가 쉽다.
| 항목 | 순수 JDBC | Spring JDBC |
|---|---|---|
| 연결 관리 | 직접 작성 | 프레임워크가 상당 부분 처리 |
| 예외 처리 | SQLException 중심 | 더 일관된 예외 변환 제공 |
| 결과 매핑 | 직접 반복 작성 | 람다/RowMapper로 단순화 |
| 코드 양 | 많아지기 쉬움 | 비교적 짧고 읽기 쉬움 |
| 학습 가치 | 데이터 접근의 본질 이해에 최고 | 실무 생산성 향상에 강함 |
여기서 중요한 것은 둘 중 하나만 옳다는 뜻이 아니라는 점이다. JDBC를 알아야 Spring JDBC가 왜 편한지 진짜로 느낄 수 있다. 기본기를 생략하고 편한 도구만 먼저 쓰면, 문제가 생겼을 때 원인을 추적하기 어렵다.
학습용 코드를 실무형 프로젝트로 바꾸는 가장 현실적인 순서
마지막으로, 지금까지 배운 내용을 실제 프로젝트 흐름으로 연결해 보자. 가장 추천하는 경로는 다음과 같다.
1단계: 콘솔 프로그램을 Maven 또는 Gradle 프로젝트로 바꾼다.
2단계: 핵심 비즈니스 로직에 JUnit 테스트를 붙인다.
3단계: H2 같은 가벼운 데이터베이스 또는 MySQL/PostgreSQL에 JDBC로 연결한다.
4단계: 반복되는 JDBC 코드를 Spring Boot +
JdbcTemplate구조로 옮긴다.5단계: 프로젝트가 더 커지면 JPA, Spring Data JPA, REST API, 보안, 배포 자동화로 확장한다.
예를 들어 작은 도서 관리 프로그램을 만든다고 해 보자. 처음에는 ArrayList에 책을 저장하는 콘솔 앱으로 시작할 수 있다. 다음에는 JUnit으로 책 등록, 가격 수정, 삭제 예외 같은 테스트를 붙인다. 그다음에는 JDBC로 books 테이블을 연결해 실제 데이터베이스에 저장한다. 이후 Spring Boot로 옮겨 GET /books, POST /books 같은 API를 만들면, 학습용 프로젝트가 금세 포트폴리오형 프로젝트로 자라난다.
이 과정에서 가장 중요한 태도는 화려한 기술 이름을 많이 아는 것이 아니다. 작동하는 작은 프로그램을, 검증 가능하고 재현 가능하며 확장 가능한 구조로 한 단계씩 바꾸는 힘이 핵심이다.
마지막 정리
자바를 공부하는 초반에는 문법과 객체지향이 가장 커 보인다. 하지만 실무로 갈수록 진짜 차이를 만드는 것은 다음 질문들이다.
이 코드는 테스트할 수 있는가?
이 프로젝트는 누구나 같은 방식으로 빌드할 수 있는가?
구조가 커져도 경계가 무너지지 않는가?
데이터베이스와 안전하게 연결되는가?
반복 작업을 줄이면서도 본질을 잃지 않는가?
JUnit은 코드의 신뢰를 높여 주고, Maven과 Gradle은 작업을 자동화하며, 모듈은 경계를 분명하게 만들고, JDBC는 데이터 접근의 본질을 보여 주며, Spring은 그 반복을 실용적으로 줄여 준다.
여기까지 왔다면, 이제 자바를 "문법으로 아는 단계"를 넘어 프로젝트로 다룰 수 있는 단계에 들어선 것이다. 학습은 여기서 끝이 아니라, 이제부터가 진짜 시작이다.
참고
3Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.