본문 바로가기
Framework/Spring Boot

[Spring Boot] Spring Initializr 실무 의존성 설계 가이드

집을 지을 때 가장 중요한 것은 화려한 인테리어가 아니라 탄탄한 설계도와 뼈대(Foundation)입니다. 스프링 부트(Spring Boot) 개발 역시 마찬가지입니다. 처음에 프로젝트 뼈대를 어떻게 잡고, 빌드 도구를 무엇으로 선택하며, 어떤 라이브러리(Dependencies)들을 담고 시작하느냐에 따라 1년 뒤 프로젝트의 유지보수 난이도가 천차만별로 갈라집니다.
다행히도 스프링 진영은 개발자들이 손쉽게 뼈대를 짤 수 있도록 Spring Initializr(start.spring.io)라는 강력한 웹 기반의 제너레이터를 제공합니다.

 

실무에서 100% 통용되는 프로젝트 구조 설계와 필수 의존성(Dependencies) 세팅 노하우에 대하여 다뤄보겠습니다.

1. Project, Language, Boot Version의 실무적 선택

Spring Initializr 웹사이트에 접속하면 가장 먼저 선택해야 할 커다란 분류들이 나타납니다.

  • Project (빌드 도구): Gradle - Groovy (강력 권장)
    과거에는 XML 기반의 Maven을 많이 썼지만, 최근 국내 실무 표준은 압도적으로 Gradle입니다. 가독성이 좋고, 빌드 속도가 Maven보다 훨씬 빠르며(캐싱 및 증분 빌드 지원), 커스텀 빌드 스크립트 작성이 용이하기 때문입니다. Kotlin DSL도 떠오르고 있지만 아직은 Groovy DSL이 가장 무난한 선택입니다.
  • Language: Java
    국내 백엔드 채용 시장의 90% 이상은 자바(Java) 기반입니다. 최근 Kotlin이 무섭게 치고 올라오고 있지만, 생태계의 압도적 크기와 레퍼런스를 고려할 때 입문자나 일반적인 실무 프로젝트는 여전히 Java 17을 권장합니다.
  • Spring Boot Version: 3.x.x (최신 안정화 버전)
    버전 옆에 (SNAPSHOT)이나 (M2) 같은 글자가 안 붙어 있는 순수한 숫자(예: 3.2.3)를 선택해야 합니다. 앞서 에서 강조했듯 3.x 버전은 Java 17 이상을 강제합니다.

 

2. 절대 빠질 수 없는 실무 5대 필수 의존성 (Dependencies)

우측의 [ADD DEPENDENCIES] 버튼을 눌러 프로젝트에 담을 무기들을 선택할 차례입니다. 실무에서 가장 흔히 쓰이는 웹/DB API 서버를 기준으로 반드시 추가해야 할 5가지를 소개합니다.

1. Spring Web (핵심)
웹 애플리케이션을 만들기 위한 알파이자 오메가입니다. 이것을 추가하면 내장 톰캣(Tomcat) 서버, Spring MVC, RESTful API를 만들기 위한 핵심 어노테이션(@RestController, @GetMapping 등), JSON 직렬화를 위한 Jackson 라이브러리가 한 번에 싹 딸려옵니다.

2. Lombok (롬복)
자바의 고질적인 단점인 길고 장황한 Getter, Setter, 생성자 코드를 단 한 줄의 어노테이션(@Getter, @RequiredArgsConstructor)으로 끝내버리는 마법의 플러그인입니다. 실무 프로젝트 100곳 중 99곳이 사용합니다.

3. Spring Data JPA (데이터베이스 연결)
과거 MyBatis로 쿼리를 한 땀 한 땀 짜던 시절을 끝내버린 혁신적인 ORM 기술입니다. 자바 객체(Entity)를 만들면 JPA가 알아서 SQL로 변환하여 DB에 날려줍니다. 쿼리를 짜는 시간을 비즈니스 로직 고민하는 시간으로 바꿔줍니다.

4. H2 Database (개발/테스트용)
메모리(In-Memory) 기반의 초경량 관계형 데이터베이스입니다. 무거운 MySQL을 깔지 않아도 스프링 부트가 구동될 때 메모리에 임시 DB를 띄우고, 서버가 꺼지면 데이터가 날아갑니다. 초기 개발이나 단위 테스트(Unit Test)를 할 때 없어서는 안 될 소중한 도구입니다.

5. Validation (유효성 검증)
클라이언트가 API로 넘긴 이메일 형식이 맞는지, 패스워드가 8자리 이상인지 일일이 if(email == null) 코드로 검증하는 것은 하수입니다. @NotBlank, @Email 어노테이션으로 우아하게 입력값을 검증(Validation)하기 위한 필수 의존성입니다.

 

3. 실무 디렉토리 패키지(Package) 구조 설계 전략

생성된 프로젝트를 인텔리제이(IntelliJ)로 열면 src/main/java 하위에 기본 패키지가 생깁니다. 실무에서는 이 패키지 구조를 어떻게 짤까요? 크게 두 가지 진영으로 나뉩니다.

1. 계층형 아키텍처 (Layered Architecture):
패키지 이름을 controller, service, repository, domain 등으로 나누고, 그 안에 모든 기능의 클래스들을 다 때려 넣는 방식입니다. 초보자가 이해하기 쉽지만, 프로젝트가 커져서 controller 패키지 안에 파일이 100개가 넘어가면 원하는 파일을 찾기 힘들어지는 지옥이 펼쳐집니다.

2. 도메인형 아키텍처 (Domain-driven Architecture) - 실무 권장:
기능(도메인)별로 패키지를 묶는 방식입니다. user, product, order라는 큰 패키지를 만들고, user 패키지 안에 UserController, UserService, UserEntity를 응집시켜 놓습니다. 내가 회원(User) 관련 코드를 수정해야 할 때 user 패키지만 열어보면 되므로 유지보수성이 극대화됩니다.

//  실무에서 가장 사랑받는 도메인 중심의 패키지 구조 예시
com.company.project
 global              // 도메인과 무관한 전역 설정 (예외 처리, 시큐리티, 공통 유틸)
    config
    error
    util
 domain              // 각 비즈니스 로직을 도메인별로 완벽히 격리
     member
        api         // MemberController
        application // MemberService
        domain      // MemberEntity, MemberRepository
     order
         api
         application
         domain

 

4. 마무리 및 주요 발전 방향

지금까지 Spring Initializr를 통해 Gradle 빌드 환경을 구축하고, 실무에서 숨 쉬듯 쓰이는 5대 필수 의존성(Lombok, JPA 등)을 챙기며, 유지보수의 꽃이라 불리는 도메인 주도 패키지 디렉토리 설계법에 대해 3,000자 분량으로 아주 치밀하게 다루어 보았습니다. 이제 집을 짓기 위한 완벽한 뼈대와 도구 상자가 완성되었습니다.

뼈대가 만들어졌으니 이제 브라우저의 요청(Request)을 받아 처리하고 응답(Response)을 내려주는 웹 서버의 심장을 만들어야 합니다. 백엔드 개발자라면 자다가도 벌떡 일어나서 설명할 수 있어야 하는 절대적인 패턴이 있습니다. 본 설명에서는 "클라이언트의 요청이 들어와서 DB를 거쳐 응답으로 나갈 때까지의 완벽한 흐름!" Spring Boot MVC 패턴 완벽 해부 (Controller, Service, Repository의 명확한 역할 분담)에 대해 아주 꼼꼼하게 파헤쳐 보겠습니다!


만약 여러분이 햄버거 매장의 점장이라고 상상해 봅시다. 카운터에서 손님의 주문(Request)을 받고 결제하는 직원이 있고, 뒤쪽 주방에서 패티를 굽고 햄버거를 조립(비즈니스 로직)하는 요리사가 있으며, 냉장고(Database)에서 신선한 식재료를 꺼내오는 창고 관리자가 있습니다. 이 세 명의 직원이 완벽하게 역할(Role)을 분담하여 일할 때 매장은 가장 효율적으로 돌아갑니다. 만약 카운터 직원이 주문을 받다 말고 주방에 들어가서 패티를 굽고 냉장고 문을 열어젖힌다면 매장은 금세 난장판이 될 것입니다. 스프링 부트(Spring Boot) 백엔드 애플리케이션도 이와 똑같습니다. 하나의 클래스 안에 모든 코드를 때려 넣는 '스파게티 코드'를 방지하고, 유지보수와 테스트를 극대화하기 위해 탄생한 아키텍처가 바로 MVC(Model-View-Controller) 패턴과 3계층 아키텍처(3-Tier Architecture)입니다.

Controller, Service, Repository의 명확한 역할 분담과 실무 규칙에 대하여 다뤄보겠습니다.

‍ 1. Controller (표현 계층 - 카운터 직원)

Controller(컨트롤러)는 매장의 카운터 직원과 같습니다. 클라이언트(브라우저, 모바일 앱)로부터 들어오는 HTTP 요청(GET, POST 등)을 가장 먼저 맞이하는 관문(Entry Point)입니다.

  • 주요 역할:
    • 사용자의 요청(Request) URL과 HTTP Method를 매핑(@GetMapping, @PostMapping)
    • 사용자가 보낸 데이터(JSON 파라미터, 쿼리 스트링)의 형식이 올바른지 유효성 검증(Validation)
    • (핵심) 주방장(Service)에게 요리 지시를 내림
    • 요리가 완성되면 손님에게 응답(Response, JSON 또는 HTML) 반환
  • 절대 금기사항: 컨트롤러 안에서 비밀번호를 암호화하거나, 데이터를 계산하는 등 비즈니스 로직(Business Logic)을 직접 짜면 절대 안 됩니다. 카운터 직원이 패티를 구우면 안 되는 것과 같습니다. 무조건 Service 클래스로 넘겨야 합니다.
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
    
    // 카운터 직원은 주방장(Service)의 연락처만 알고 있습니다.
    private final UserService userService;

    @PostMapping("/signup")
    public ResponseEntity<String> signup(@RequestBody @Valid UserSignupDto dto) {
        // 컨트롤러는 요리(로직)를 하지 않고, Service에게 데이터(DTO)만 툭 던지며 지시합니다.
        userService.registerUser(dto); 
        return ResponseEntity.ok("회원가입 성공!"); // 그리고 결과를 손님에게 반환합니다.
    }
}

‍ 2. Service (비즈니스 계층 - 주방장)

Service(서비스)는 매장의 주방장입니다. 애플리케이션의 가장 핵심적인 뇌(Brain)이자 비즈니스 로직이 살아 숨 쉬는 곳입니다. 트랜잭션(Transaction)이 시작되고 끝나는 곳이기도 합니다.

  • 주요 역할:
    • 컨트롤러가 넘겨준 데이터를 바탕으로 실질적인 계산, 검증, 상태 변경 등의 핵심 비즈니스 로직 수행
    • "이미 가입된 이메일인지 검사하라", "비밀번호를 암호화하라", "회원 등급을 올려라" 등등
    • (핵심) 데이터가 필요하면 창고 관리자(Repository)에게 식재료(Data)를 요청함
  • 실무 팁: 서비스 코드는 최대한 스프링 프레임워크나 HTTP 기술에 종속되지 않게 순수한 자바(Java) 코드로 유지하는 것이 테스트하기 가장 좋습니다. 서비스 계층에 HttpServletRequest 같은 웹 계층 객체가 파라미터로 넘어오면 설계가 꼬인 것입니다.
@Service
@RequiredArgsConstructor
public class UserService {
    
    // 주방장은 창고 관리자(Repository)의 연락처를 알고 있습니다.
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder; // 암호화 도구

    @Transactional
    public void registerUser(UserSignupDto dto) {
        // 비즈니스 로직 1: 이메일 중복 검사
        if (userRepository.existsByEmail(dto.getEmail())) {
            throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
        }
        
        // 비즈니스 로직 2: 비밀번호 암호화 및 유저 객체 생성
        String encodedPassword = passwordEncoder.encode(dto.getPassword());
        UserEntity newUser = UserEntity.builder()
                .email(dto.getEmail())
                .password(encodedPassword)
                .build();
                
        // 창고 관리자에게 저장을 부탁함
        userRepository.save(newUser);
    }
}

3. Repository (데이터 접근 계층 - 창고 관리자)

Repository(리포지토리)는 냉장고와 창고(Database)를 관리하는 직원입니다. 데이터를 저장(Insert)하고, 찾고(Select), 수정하고(Update), 삭제하는(Delete) 순수한 데이터베이스 통신 역할만 전담합니다.

  • 주요 역할: 데이터베이스(MySQL, Oracle 등)와 직접 연결되어 쿼리(Query)를 수행합니다.
  • 특징: 과거에는 SQL 쿼리를 길게 직접 짰지만, 현대의 Spring Data JPA를 사용하면 인터페이스(Interface)만 선언해 둬도 스프링이 알아서 창고 관리자 객체를 만들어 줍니다.
// JpaRepository를 상속받는 것만으로 기본 CRUD(저장, 조회, 삭제) 기능이 완성됩니다.
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    // 메서드 이름만 잘 지어도 JPA가 알아서 SQL(SELECT count(*) FROM users WHERE email=?)로 번역해 줍니다!
    boolean existsByEmail(String email);
}

4. 마무리 및 주요 발전 방향

지금까지 복잡한 웹 애플리케이션의 코드를 3등분하여, 카운터 직원(Controller)이 요청을 받고, 주방장(Service)이 핵심 로직을 처리하며, 창고 관리자(Repository)가 DB와 소통하는 '3-Tier 아키텍처의 완벽한 분업화'에 대해 밀도 있게 다루어 보았습니다. 이 원칙만 철저히 지켜도 여러분의 코드는 스파게티가 되지 않고 깔끔하게 유지보수될 수 있습니다.

 

코드의 구조는 완벽해졌습니다. 그런데 프로젝트를 진행하다 보니 데이터베이스 접속 정보, 포트 번호, 외부 API 키 등 '설정값'들을 저장해야 할 일이 생겼습니다. 이 설정값들을 자바 코드 안에 하드코딩(Hard-coding)하면 로컬 개발 환경과 실제 상용(Production) 서버 환경이 달라서 빌드할 때마다 코드를 고쳐야 하는 끔찍한 사태가 발생합니다.

 

다음 글에서는 "로컬 서버와 상용 서버의 환경을 단 한 줄의 코드로 스위칭(Switching)한다!" application.yml 파일의 마법과 프로필(Profile)을 활용한 완벽한 환경 분리 실무 전략에 대해 다뤄보겠습니다.