Java 콘솔 프로젝트, 왜 구조가 중요할까요?
Java 콘솔 프로젝트를 처음 접하는 분들이 가장 많이 겪는 어려움 중 하나가 바로 ‘어떻게 시작해야 할까?’ 입니다. 단순히 몇 개의 .java 파일만으로도 간단한 프로그램은 만들 수 있지만, 프로젝트 규모가 조금만 커져도 금세 복잡해지고 관리하기 어려워집니다. 마치 집을 지을 때 설계도 없이 벽돌만 쌓아 올리는 것과 같습니다.
잘 설계된 프로젝트 구조는 다음과 같은 이점을 제공합니다.
-
가독성 향상: 코드를 찾고 이해하기 쉬워집니다.
-
유지보수 용이성: 버그 수정이나 기능 추가가 훨씬 수월해집니다.
-
확장성 확보: 새로운 기능을 추가하거나 기존 기능을 변경할 때 유연하게 대처할 수 있습니다.
-
협업 효율 증대: 여러 사람이 함께 작업할 때 혼란을 줄이고 생산성을 높입니다.
특히 초보자일수록 처음부터 좋은 구조를 익혀두는 것이 장기적으로 실력 향상에 큰 도움이 됩니다. 이 글에서는 초보자도 쉽게 이해하고 적용할 수 있는 Java 콘솔 프로젝트 구조 설계 방법을 자세히 알려드리겠습니다.
1단계: 프로젝트의 기본 뼈대 만들기
어떤 프로젝트든 시작은 ‘기본 뼈대’를 만드는 것에서부터 출발합니다. Java 콘솔 프로젝트에서는 크게 세 가지 영역으로 나누어 생각하면 좋습니다.
-
소스 코드(Source Code): 실제 프로그램 로직이 담기는 부분입니다.
-
리소스(Resource): 설정 파일, 데이터 파일 등 프로그램 실행에 필요한 외부 파일들을 모아둡니다.
-
테스트 코드(Test Code): 작성한 코드가 제대로 작동하는지 검증하는 부분입니다.
이 세 가지를 염두에 두고, 일반적인 폴더 구조를 만들어 보겠습니다. IDE(통합 개발 환경)를 사용하신다면 프로젝트 생성 시 기본적인 틀을 제공해주지만, 그 구조를 이해하고 활용하는 것이 중요합니다.
1.1. 최상위 디렉토리
프로젝트 이름으로 된 최상위 디렉토리를 만듭니다. 예를 들어, MyConsoleApp이라는 프로젝트라면 MyConsoleApp 디렉토리가 최상위가 됩니다.
MyConsoleApp/
1.2. 소스 코드 디렉토리 (src)
Java 소스 코드는 일반적으로 src라는 이름의 디렉토리에 모아둡니다. 이 src 디렉토리 안에는 다시 main과 test 디렉토리를 만들어 구분하는 것이 일반적인 관례입니다.
MyConsoleApp/
└── src/
├── main/
└── test/
-
src/main/: 실제 실행될 프로그램의 소스 코드를 담는 곳입니다. -
src/test/: 테스트 코드를 담는 곳입니다.
1.3. Java 소스 코드 패키지 구조 (src/main/java)
src/main 디렉토리 안에는 실제 Java 소스 파일(.java)들이 위치할 java 디렉토리를 만듭니다. 그리고 이 java 디렉토리 안에서 패키지(Package)를 사용하여 코드를 체계적으로 관리합니다.
패키지란? Java에서는 클래스들을 논리적으로 묶어 관리하는 방법을 제공하는데, 이를 패키지라고 합니다. 마치 컴퓨터의 폴더처럼, 관련된 클래스들을 같은 패키지 안에 넣어두면 코드를 찾기 쉽고 이름 충돌을 방지할 수 있습니다.
초보자를 위한 패키지 구조 예시:
처음에는 간단하게 시작하는 것이 좋습니다. 프로젝트의 주요 기능별로 패키지를 나누거나, 역할별로 나누는 방법을 고려해볼 수 있습니다.
-
com.example.myapp.main: 프로그램의 메인 진입점(main메소드)을 포함하는 클래스 -
com.example.myapp.service: 핵심 비즈니스 로직을 처리하는 클래스 -
com.example.myapp.util: 여러 곳에서 공통으로 사용되는 유틸리티 클래스 -
com.example.myapp.domain: 데이터 모델이나 객체를 정의하는 클래스 (필요하다면)
주의: 패키지 이름은 일반적으로 회사의 도메인 이름을 거꾸로 사용하고, 프로젝트 이름을 붙이는 방식으로 만듭니다. 예를 들어 com.google.myapp 또는 org.yourcompany.projectname과 같이 작성합니다. 초보자라면 com.example.myapp 정도로 시작해도 좋습니다.
MyConsoleApp/
└── src/
└── main/
└── java/
└── com/
└── example/
└── myapp/
├── main/ <-- 메인 클래스 (예: App.java)
├── service/ <-- 서비스 클래스
├── util/ <-- 유틸리티 클래스
└── domain/ <-- 데이터 모델 클래스 (선택 사항)
1.4. 리소스 디렉토리 (src/main/resources)
프로그램 실행에 필요한 설정 파일(properties, xml, json 등)이나 데이터 파일 등이 있다면 src/main/resources 디렉토리에 배치합니다.
MyConsoleApp/
└── src/
└── main/
├── java/
│ └── com/
│ └── example/
│ └── myapp/
│ ├── main/
│ ├── service/
│ └── util/
└── resources/ <-- 설정 파일, 데이터 파일 등
└── config.properties
1.5. 테스트 코드 디렉토리 (src/test/java)
테스트 코드는 src/test/java 디렉토리 아래에, 실제 소스 코드와 동일한 패키지 구조를 유지하며 작성하는 것이 일반적입니다. 이렇게 하면 어떤 클래스에 대한 테스트 코드인지 쉽게 파악할 수 있습니다.
MyConsoleApp/
└── src/
├── main/
│ └── java/
│ └── com/
│ └── example/
│ └── myapp/
│ ├── main/
│ ├── service/
│ └── util/
└── test/
└── java/
└── com/
└── example/
└── myapp/
├── main/ <-- 메인 클래스 테스트
└── service/ <-- 서비스 클래스 테스트
2단계: 프로젝트 빌드 및 관리 도구 활용
간단한 프로젝트라면 IDE만으로도 충분하지만, 프로젝트가 커지거나 의존성(다른 라이브러리 사용)이 생기면 빌드 도구의 도움이 필수적입니다. Java 생태계에서는 Maven과 Gradle이 가장 널리 사용됩니다.
2.1. Maven
Maven은 프로젝트 객체 모델(Project Object Model, POM)이라는 XML 파일을 기반으로 프로젝트를 관리합니다. pom.xml 파일에 프로젝트 정보, 의존성, 빌드 설정 등을 정의하면 Maven이 알아서 라이브러리를 다운로드하고 컴파일, 테스트, 패키징 등의 작업을 수행합니다.
Maven 프로젝트 구조 (기본):
Maven은 자체적으로 표준적인 디렉토리 구조를 권장합니다. 위에서 설명한 src/main/java, src/main/resources, src/test/java 등의 구조가 바로 Maven 표준 구조입니다.
MyConsoleApp/
├── pom.xml <-- Maven 설정 파일
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── myapp/
│ │ ├── main/
│ │ ├── service/
│ │ └── util/
│ └── resources/
│ └── config.properties
└── test/
└── java/
└── com/
└── example/
└── myapp/
├── main/
└── service/
pom.xml 예시:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-console-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- 의존성 추가 -->
<dependencies>
<!-- JUnit 테스트 라이브러리 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- Lombok (선택 사항: getter, setter 자동 생성) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 컴파일러 설정 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- 테스트 실행 설정 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
Maven 명령어:
-
mvn clean install: 프로젝트를 클린하고 빌드, 테스트, 패키징까지 수행합니다. -
mvn test: 테스트 코드를 실행합니다. -
mvn package: 실행 가능한 JAR 파일 등을 생성합니다.
2.2. Gradle
Gradle은 Maven보다 더 유연하고 빠른 빌드 도구로, Groovy 또는 Kotlin DSL을 사용하여 빌드 스크립트(build.gradle 또는 build.gradle.kts)를 작성합니다. Maven과 마찬가지로 의존성 관리, 컴파일, 테스트, 패키징 등 다양한 빌드 작업을 자동화합니다.
Gradle 프로젝트 구조 (기본):
Gradle도 Maven과 유사한 표준 디렉토리 구조를 따릅니다.
MyConsoleApp/
├── build.gradle <-- Gradle 설정 파일
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── myapp/
│ │ ├── main/
│ │ ├── service/
│ │ └── util/
│ └── resources/
│ └── config.properties
└── test/
└── java/
└── com/
└── example/
└── myapp/
├── main/
└── service/
build.gradle (Groovy DSL) 예시:
plugins {
id 'java'
id 'application' // 메인 클래스 설정을 위한 플러그인
id 'org.projectlombok' version '0.10.0' // Lombok 플러그인
}
group 'com.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
// JUnit 테스트 라이브러리
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
// Lombok
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// 메인 클래스 설정 (application 플러그인 사용 시)
application {
mainClass = 'com.example.myapp.main.App'
}
// 테스트 실행 설정
test {
useJUnitPlatform()
}
Gradle 명령어:
-
gradle build: 프로젝트를 빌드합니다. -
gradle test: 테스트 코드를 실행합니다. -
gradle run:application플러그인에 설정된 메인 클래스를 실행합니다.
어떤 도구를 선택해야 할까요?
-
Maven: 역사가 길고 커뮤니티 지원이 탄탄합니다. XML 기반이라 익숙한 분들이 많습니다.
-
Gradle: 더 유연하고 빌드 속도가 빠르다는 장점이 있습니다. Groovy/Kotlin DSL로 빌드 스크립트를 작성하여 가독성이 좋습니다.
초보자라면 둘 중 하나를 선택하여 익숙해지는 것이 중요합니다. IDE에서 프로젝트를 생성할 때 Maven 또는 Gradle을 선택하는 옵션이 있으니, 이를 활용해 보세요.
3단계: 흔한 실수와 피해야 할 점
좋은 프로젝트 구조를 만들고자 할 때, 초보자들이 흔히 저지르는 몇 가지 실수가 있습니다.
3.1. 모든 것을 main 패키지에 넣기
프로젝트를 처음 시작할 때, 모든 클래스를 com.example.myapp.main과 같은 하나의 패키지에 넣는 경우가 많습니다. 이렇게 하면 초기에는 편할 수 있지만, 클래스가 몇 개만 늘어나도 코드를 찾기 어렵고 뒤죽박죽이 됩니다.
해결책: 위에서 설명한 대로, 기능별 또는 역할별로 패키지를 분리하여 관리하는 습관을 들이세요. service, util, controller (웹 프로젝트의 경우) 등으로 나누는 것이 좋습니다.
3.2. 패키지 이름 규칙 무시
패키지 이름은 단순히 폴더 이름이 아니라, 코드의 네임스페이스(Namespace) 역할을 합니다. .java 파일 상단에 package com.example.myapp;와 같이 선언되며, 이를 통해 클래스의 고유한 위치를 지정합니다.
피해야 할 패키지 이름:
-
test,util,helper등 너무 일반적인 이름 -
숫자로 시작하는 이름 (Java 식별자 규칙 위반)
-
대문자를 포함하는 이름 (일반적으로 소문자 사용)
권장: com.yourcompany.yourproject.subcomponent와 같이 계층적이고 의미 있는 이름을 사용하세요.
3.3. 리소스 파일을 src/main/java에 넣기
설정 파일(.properties, .yml)이나 데이터 파일(.csv, .json) 등은 .java 파일이 아닙니다. 따라서 src/main/java 디렉토리가 아닌 src/main/resources 디렉토리에 배치해야 합니다. 빌드 도구는 src/main/resources에 있는 파일들을 자동으로 클래스패스(Classpath)에 포함시켜주기 때문에, 프로그램 실행 중에 해당 파일들에 쉽게 접근할 수 있습니다.
3.4. 테스트 코드 작성의 중요성 간과
프로젝트 구조에서 src/test 디렉토리를 만들었지만, 실제 테스트 코드를 작성하지 않는 경우가 많습니다. 하지만 테스트 코드는 프로그램의 안정성을 보장하고, 리팩토링(코드 개선) 시 오류 발생을 줄여주는 매우 중요한 역할을 합니다.
팁: 간단한 기능이라도 단위 테스트(Unit Test)를 작성하는 습관을 들이세요. JUnit과 같은 테스트 프레임워크를 활용하면 좋습니다.
3.5. 과도한 패키지 분리
반대로, 너무 세세하게 패키지를 나누는 것도 좋지 않습니다. 하나의 .java 파일에 수십 개의 클래스가 있는 것이 문제라면, 하나의 패키지에 클래스가 너무 많아도 문제가 될 수 있습니다. 하지만 각 클래스가 몇 줄 되지 않는 간단한 콘솔 애플리케이션에서 com.example.myapp.service.user, com.example.myapp.service.product 등으로 나누는 것은 과도할 수 있습니다.
균형: 프로젝트의 규모와 복잡성을 고려하여 적절하게 패키지를 분리하는 것이 중요합니다. 처음에는 몇 개의 큰 패키지로 시작하고, 필요에 따라 더 세분화하는 방식을 추천합니다.
4단계: 실용적인 프로젝트 구조 예시 (콘솔 앱)
이제까지 배운 내용을 바탕으로, 간단한 콘솔 애플리케이션의 프로젝트 구조를 구체적으로 살펴보겠습니다.
예시 시나리오: 사용자 이름을 입력받아 환영 메시지를 출력하는 간단한 Java 콘솔 애플리케이션
SimpleConsoleApp/
├── pom.xml (또는 build.gradle)
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── simpleapp/
│ │ ├── main/
│ │ │ └── App.java <-- 프로그램 시작점 (main 메소드)
│ │ ├── service/
│ │ │ └── GreetingService.java <-- 환영 메시지 생성 로직
│ │ └── util/
│ │ └── InputUtil.java <-- 사용자 입력 처리 유틸리티
│ └── resources/
│ └── banner.txt <-- (선택 사항) 시작 시 출력할 배너 파일
└── test/
└── java/
└── com/
└── example/
└── simpleapp/
├── service/
│ └── GreetingServiceTest.java <-- GreetingService 테스트
└── util/
└── InputUtilTest.java <-- InputUtil 테스트
각 파일의 역할:
-
App.java(com.example.simpleapp.main) -
public static void main(String[] args)메소드를 포함합니다. -
프로그램의 시작점 역할을 하며, 필요한 서비스나 유틸리티를 초기화하고 호출합니다.
-
로직이 복잡해지지 않도록, 실제 작업은 다른 클래스(서비스, 유틸리티 등)에 위임하는 것이 좋습니다.
package com.example.simpleapp.main;
import com.example.simpleapp.service.GreetingService;
import com.example.simpleapp.util.InputUtil;
public class App {
public static void main(String[] args) {
System.out.println("--- Simple Console App ---");
// Optional: Load banner from resources
// InputUtil.displayBanner("banner.txt");
String name = InputUtil.getUserName();
String message = GreetingService.generateGreeting(name);
System.out.println(message);
System.out.println("------------------------");
}
}
-
GreetingService.java(com.example.simpleapp.service) -
핵심 비즈니스 로직을 담당합니다. 여기서는 사용자 이름으로 환영 메시지를 만드는 로직을 구현합니다.
-
단일 책임 원칙(Single Responsibility Principle)에 따라, 하나의 클래스는 하나의 책임만 갖도록 설계하는 것이 좋습니다.
package com.example.simpleapp.service;
public class GreetingService {
public static String generateGreeting(String name) {
if (name == null || name.trim().isEmpty()) {
return "안녕하세요, 방문자님!";
}
return "안녕하세요, " + name + "님!";
}
}
-
InputUtil.java(com.example.simpleapp.util) -
반복적으로 사용될 수 있는 유틸리티 기능을 제공합니다. 여기서는 사용자로부터 이름을 입력받는 기능을 구현합니다.
-
Scanner와 같은 Java 표준 라이브러리를 활용하여 입력을 처리합니다.
“`java
package com.example.simpleapp.util;
import java.util.Scanner;
public class InputUtil {
private static Scanner scanner = new Scanner(System.in);
public static String getUserName() {
System.out.print(“당신의 이름을 입력해주세요: “);
return scanner.nextLine();
}
// Optional: Method to display banner from resources
// public static void displayBanner(String resourceName) {
// try (var is = InputUtil.class.getClassLoader().getResourceAsStream(resourceName)) {
// if (is != null
INTERNAL_LINKS: (유사한 게시글 입력)
