엔터프라이즈 애플리케이션의 표준, Spring Boot

AngJ·2026년 2월 1일
post-thumbnail

개요

백엔드 개발의 핵심은 데이터를 어떻게 안전하게 저장하고, 효율적으로 처리하여, 사용자에게 전달할 것인가 에 있습니다. 다른 백엔드 개발 프레임워크들(Django, Node.js)등도 있지만, Spring Boot가 채택이 되는 이유는 대규모 엔터프라이즈 환경에서의 확실한 안정성과 정형화된 규칙, 예외처리, 인증 및 인가에서 강력한 기능을 제공하기 때문입니다. 따라서 여러 사람이 동일한 프로젝트를 개발하여도 일률적인 규칙에 의해 개발이 가능하고 안정적인 서비스 제공이 가능합니다.

1️⃣ 견고한 기초: 객체지향과 설계 원칙 (Java OOP & SOLID)

Spring Boot에 대해 알아보기 전, Spring Boot의 근간이 되는 언어인 Java의 중요 개념에 대해 알아보고자 합니다.
Java는 객체지향 언어로 그 특징으로 캡슐화, 추상화, 다형성, 상속을 가집니다.

  • 캡슐화 : 객체 외부에서 내부에 어떤 필드를 가지고 있는지 알지 못하게 하는 것입니다.
  • 추상화: 메서드의 동작을 선언하지 않고 메서드명만 선언할 수 있는 것입니다.
  • 다형성: 같은 메서드명을 가졌지만, 다양한 동작을 하는 메서드를 가질 수 있는 것입니다.
  • 상속: 자식 객체는 부모 객체의 필드 및 메서드를 가질 수 있는 것입니다.

캡슐화는 접근 제어자, 추상화는 abstract class, interface, 다형성은 method overriding, method overloading, 상속은 extends를 이용해 구현합니다.

다음으로 객체지향의 5대 원칙인 SOLID 원칙입니다.

  • SRP (단일 책임 원칙)
    : 하나의 클래스는 하나의 책임만을 가져야 한다.
  • OCP (개방 폐쇄 원칙)
    : 클래스는 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
  • LSP (리스코프 치환 원칙)
    : 부모 클래스를 자식 클래스가 대체할 수 있어야 한다.
  • ISP (인터페이스 치환 원칙)
    : 하나의 범용적 인터페이스가 아닌 여러 개의 인터페이스로 구현해야 한다.
  • DIP (의존 역전 원칙)
    : 고수준 모듈은 저수준 모듈에 의존하면 안된다.
    (상위 모듈은 하위 모듈이 아닌 인터페이스에 의존해야 한다.)

결국 Spring Boot는 위와 같은 Java의 객체 지향 특징과 원칙에 따라 구현되어야 합니다.

2️⃣ 생산성의 혁신: Spring Boot 아키텍처 (Core & Container)

Spring Boot를 공부하기 전 Spring Boot의 전신인 Spring과 어떤 차이점이 있는지 알아보고자 합니다.
Spring은 동일하게 백엔드 개발에 사용되던 Java 기반 프레임워크입니다. 하지만, 이를 사용하기 위해선 개발에 사용되던 시간보다 설정(환경, 의존성, 서버 실행 등)에 사용되는 시간이 많았습니다.. 이 점 때문에 백엔드 개발의 강력한 프레임워크이지만 사용에 어려움이 있었고, 이를 해결하기 위해 탄생한 것이 Spring Boot입니다.

  • “설정하는 시간을 줄이고, 개발에 할애하는 시간을 늘이자!”

Spring Framework의 복잡한 설정을 Spring Boot는 IoC와 DI, Bean을 통해 자동화합니다.

IoC는 제어의 역전으로 객체의 생성, 생명 주기 관리, 의존성 주입 등을 개발자가 아닌 Spring 컨테이너가 관리하도록 위임하는 것을 의미합니다. 따라서 코드의 결합도를 낮추고, 유연한 아키텍처와 테스트 가능한 구조로 만들 수 있습니다.

이처럼 컨테이너가 외부에서 객체를 관리해주기 때문에 개발자는 특정 기술이나 프레임워크에 종속되지 않은 순수한 자바 객체(POJO (Plain Old Java Object))만으로 비즈니스 로직을 구현할 수 있습니다. 이를 통해 객체지향 원리에 충실한 설계를 가능하게 하며, 단위 테스트를 더욱 용이하게 만듭니다.

의존성 주입(DI)은 이렇게 관리되는 POJO 객체들이 의존하는 다른 객체를 직접 생성하는 것이 아닌 Spring 컨테이너에서 주입받는 방식을 말합니다. Spring에서는 생성자 주입을 사용해 불변성을 보장하고 순환 참조를 방지하는 것이 권장됩니다.

생성자 주입 방식이 권장되는 이유는 주입 받을 객체를 final로 선언해 불변성을 보장하고, 의존성 주입을 하지 않거나 순환 참조가 발생했을 시, 컴파일 타임에 이를 확인하고 수정이 가능합니다. 또한, 한눈에 파악하기 용이합니다.

DI가 적용되는 흐름은 다음 사진과 같습니다.

Spring Boot는 객체를 Bean으로 등록하여 객체를 생성하고 삭제합니다. 객체 생성 후 @PostConstruct로 초기화하고, 소멸 전 @PreDestroy로 리소스를 정리합니다. 이렇게 Bean으로 등록해 객체의 생성과 삭제를 맡기는 이유는 개발자가 객체를 생성한 후 삭제하는 로직을 작성하지 않았을 시, 메모리 누수가 발생하여 서비스 중단되는 일이 발생할 수 있는데 이를 미연에 방지하기 위해 Spring Boot는 IoC를 통해 개발자가 아닌 프레임워크에 이 일을 위임합니다.

Bean 생명 주기 흐름은 아래와 같습니다.

Spring Boot에선 서비스의 환경(Configuration)을 application.yml 파일을 이용해 설정하는데 동일한 서비스를 여러 환경에서 구동할 수 있도록 Spring Boot는 Profile 기능을 제공합니다. 이를 통해 개발 환경, 테스트 환경, 운영 환경을 코드 변경없이 전환할 수 있습니다.

이는 application-dev.yml, application-test.yml, application-prod.yml 과 같이 환경 설정 파일을 생성하고 각각의 운영환경을 활용해 구동할 수 있는 쉘 스크립트를 짠다면, Profile 기능을 활용해 환경 전환이 가능합니다.

3️⃣ 웹 서비스의 표준: REST API와 MVC (Web Layer)

Spring Boot는 요청을 중앙에서 처리하는 Dispatcher Servlet을 중심으로 Controller(요청 처리), Service(비즈니스 로직), Repository(DB 접근)로 역할을 분리해 유지보수성을 높입니다. 이러한 MVC 패턴은 데이터 처리, 화면 처리, 제어 로직을 분리하는 것이 목적입니다.

Controller는 사용자 요청의 입구로 파라미터를 받고 비즈니스 로직을 호출한 뒤 결과를 반환합니다.

Model은 Controller가 로직 수행 결과를 View에 전달할 때 사용하는 데이터 저장소입니다.

View는 사용자에게 보여줄 최종 화면(HTML, JSON 등)을 처리합니다.

계층 간의 분리를 통해 각 컴포넌트는 맡은 역할에 집중하는데 Service는 실질적 비즈니스 로직을 수행하고 Repository는 DB(데이터 저장소)에 접근하는 통로, 이후 설명할 Mapper는 자바 객체와 SQL 쿼리를 연결하는 역할을 수행합니다.

이렇게 분리된 구조를 기반으로 웹 요청을 처리하고, 데이터를 전달하며 화면을 렌더링합니다.

Spring MVC 요청 흐름은 아래와 같습니다.

웹 어플리케이션 개발을 위해선 먼저 Spring Boot의 의존성에 spring-boot-starter-web을 추가해야합니다. spring-boot-starter-web은 웹 어플리케이션에 필요한 모든 라이브러리를 한 번에 포함하는 starter로 내장 서버인 Tomcat을 포함해 프론트엔드와의 소통을 위한 RESTful API, 웹 MVC, JSON, 예외 처리, 로깅 등을 지원합니다.

Spring Boot에서 처리된 자원들을 프론트엔드에 서빙하기 위해서 일종의 규약이 필요한데 그것이 RESTful API 입니다. RESTful API는 자원을 명사(URI)로 행위를 HTTP 메서드(GET, POST 등)로 표현하는 아키텍처 스타일입니다.

GET은 조회, POST는 생성, PUT은 전체 수정, PATCH는 부분 수정, DELECT는 삭제를 의미합니다. 이렇게 표현된 규약에 따라 API를 사용하여 프론트엔드와의 통신을 하고 의미를 명확하게 전달합니다.

프론트엔드와의 원활한 통신을 위해 주로 JSON 포맷을 사용합니다. Spring Boot에서 처리한 자원을 Controller에서 @RestController 어노테이션을 이용하면 자원을 XML또는 JSON 형식으로 전달합니다.

이렇게 서빙된 API를 프론트엔드 측에서 쉽게 확인하고 테스트할 수 있도록 돕는 도구를 OAS(OpenAPI Specification)이라고 합니다. 현업에서 많이 사용되는 것은 Swagger로 Spring Boot의 springdoc-openapi 의존성을 활용해 코드로부터 API 명세서(Swagger UI)를 자동 생성해 프론트엔드와 효율적으로 협업합니다.

4️⃣ 데이터 중심 개발: JPA와 트랜잭션 (Persistence Layer)

기존의 Java를 이용한 개발에서는 Java에서 SQL을 직접 실행하여 DB와 연동하는 방식인 JDBC를 이용해 개발했습니다. JdbcTemplate 또는 MyBatis를 이용해 SQL을 직접 작성하고 모든 쿼리를 직접 작성해야했으나 이 어려움을 JPA (Java Persistence API)라는 새로운 기술을 통해 단순화하였습니다. JPA는 SQL을 직접 작성하는 JDBC 방식에서 벗어나, 객체 중심으로 데이터를 다룹니다.

JPA (ORM, Object-Relational Mapping)를 이용해 Java 객체와 DB 테이블을 매핑합니다. @Entity, @Id, @Column 등의 어노테이션으로 스키마를 정의하고, JpaRepository 인터페이스를 통해 기본적으로 CRUD를 자동합니다.

spring-boot-starter-data-jpa 의존성을 gradle 파일에 추가하고 JPA 및 DB 연결 정보를 application.yml 파일에 작성해 사용합니다.

JPA는 Spring Data JPA가 제공하는 인터페이스인 JPA Repository를 이용해 id를 이용한 데이터 추출(findById), 모든 데이터 조회(findAll) 등의 데이터베이스 조작이 가능합니다. 기본 제공되는 메서드 이외의 개발자가 메서드 키워드를 이용해 원하는 데이터를 가지고오는 메서드를 생성할 수 있습니다.

By(뒤에 오는 조건 필드 지정), And/Or(조건 연결), Like(SQL의 like와 동일한 기능)등의 메서드 키워드를 메서드에 작성하면 해당 동작을 하는 쿼리문이 자동 적용됩니다. 복잡한 조건 또는 특정 데이터 형태의 조회가 필요하다면 @Query 어노테이션을 통해 JPQL 또는 Native SQL을 이용한 쿼리문 작성도 가능합니다. 하지만 더 나아가 복잡한 동적 쿼리나 타입 안정성이 필요할 때는 자바 코드로 쿼리를 짜는 QueryDSL을 도입하는 것이 일반적입니다.

@Query 어노테이션을 사용하면 직접 쿼리문을 작성할 수 있어 편리하지만, 쿼리가 문자열 형태이기 떄문에 오타가 발생해도 컴파일 시점에 잡아내기 어렵다는 단점이 있습니다. 이를 해결하기 위해 QueryDSL을 함께 사용합니다.

즉, 간단한 조회는 메서드 이름 규칙으로 해결하고, 복잡한 동적 쿼리는 QueryDSL을 사용해 타입 안정성을 확보합니다.

JPA는 자동으로 트랜잭션을 관리해줍니다. 서비스 계층에서 @Transactional 어노테이션을 통해 데이터 정합성을 보장합니다. 예외 발생 시 자동으로 롤백(Rollback)되며, 서비스 계층에서 트랜잭션의 범위를 설정하는 것이 중요합니다. JPA에서 트랜잭션이 커밋되는 시점은 기본적으로 트랜잭션을 시작한 메서드가 성공적으로 종료될 때입니다.

트랜잭션이 시작되면 JPA는 영속성 컨텍스트라는 논리적 영역을 생성하고 엔티티는 해당 영역에, SQL은 쓰기 지연 SQL 저장소에 저장됩니다. 트랜잭션이 종료될 때, 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화(Flush)하고 쓰기 지연 저장소의 내용을 불러와 SQL을 실행합니다.

5️⃣ REST API 보안과 인증: JWT (JSON Web Token)

기존의 세션 기반 인증 방식은 서버 메모리에 사용자 상태를 저장해야 하므로 사용자가 늘어날수록 서버에 부담이 가고, 여러 대의 서버를 운영할 때 세션을 공유하기 까다롭다는 단점이 있습니다. 이를 해결하기 위해 토큰 기반 인증 방식인 JWT를 도입해 확장성을 확보합니다.

JWT는 인증에 필요한 정보들을 JSON 객체에 담아 암호화한 후, 이를 클라이언트에게 전달하는 토큰입니다. 서버는 사용자의 인증 상태를 저장하지 않고(Stateless) 클라이언트가 요청을 보낼 때마다 토큰을 함께 전달하면 서버는 토큰의 유효성만 검증하기에 서버 확장 시, 매우 유리합니다. 또한 토큰 자체에 사용자 식별 정보나 권한 등 필요한 데이터가 포함되어 있어 별도의 데이터 조회가 줄어듭니다.

JWT는 .을 구분자로 사용해 세 부분으로 구성됩니다.

Header에는 토큰의 타입(JWT)과 사용된 암호화 알고리즘 정보(HS256, RS256 등)가 담깁니다.

Payload에는 실제 전달할 사용자 정보인 Claims가 담겨있고 이 안에는 사용자 ID, 토큰 만료 시간 등을 포함합니다. Payload를 수정하더라도 서버의 Secret Key가 없으면 올바른 서명을 생성할 수 없기에 보안은 유지되지만, Payload는 암호화가 아닌 인코딩된 정보이기에 누구나 정보를 볼 수 있습니다.
따라서 비밀번호와 같은 민감 정보는 절대 담지 않도록 주의가 필요합니다.

마지막으로 Signature엔 헤더와 페이로드를 합친 후 서버만이 가진 Secret Key로 암호화한 값입니다. 이를 통해 토큰이 중간에 변조되지 않았음을 증명합니다.

동작 방식은 클라이언트가 로그인을 성공하면 서버는 JWT를 생성해 응답합니다. 이후 클라이언트는 HTTP 요청 헤더에 해당 토큰을 실어 보내고, 서버는 Secret Key를 이용해 서명을 검증하는 과정을 통해 사용자를 인증합니다.

참고) CORS(Cross-Origin Resource Sharing)
웹 브라우저는 보안 상의 이유로 다른 Origin의 리소스 요청을 기본적으로 차단하는데 프론트엔드와의 연결을 위해 서버 측에서 이를 풀어줘야합니다. 이때 사용하는 것이 CORS로 서버가 ‘내 자원을 특정 외부 도메인에서 접근해도 된다’고 허용해주는 것입니다.

6️⃣ 안정성과 운영: 횡단 관심사와 모니터링 (Aspects & Ops)

Spring boot에선 비즈니스 로직 외의 공통 관심사를 분리하고 시스템 상태를 진단합니다.

AOP (관점 지향 프로그래밍)를 이용해 로깅, 보안, 트랜잭션 등 반복되는 코드 즉, 공통 관심사를 Aspect로 모듈화하여 핵심 로직과 분리합니다. 이는 핵심 비즈니스 로직을 오염시키지 않으면서도 시스템 전반에 일관된 운영 정책을 적용할 수 있게 합니다.

OOP는 ‘기능’ 단위로 클래스를 구현한다면, AOP는 ‘공통 관심사(Aspect)’를 모듈화해 코드 중복을 줄이고 유지보수가 쉽도록 구현합니다.

이는 별도의 설정 없이 spring-boot-starter-aop 의존성만 추가한다면 바로 사용 가능합니다. 이때 제공되는 어노테이션은 @Aspect, @Advice, @JoinPoint, @Pointcut 등이 있습니다.

서버 운영 측면에서 사용자가 잘못된 값을 입력하지 않도록 사용자의 입력값 검증 또한 중요합니다. 검증 과정 없이 잘못된 데이터가 시스템으로 들어오면, 예기치 못한 오류나 데이터베이스에 원치 않는 값이 저장될 수 있기 때문입니다. 이를 위해 자바 표준 어노테이션인 @Valid 또는 Spring이 제공하는 @Validation 어노테이션을 사용합니다.

참고) @Valid는 일반적인 간단한 객체 검증에 사용되고, @Validation은 그룹을 지정해 상황별로 다르게 검증할 때 사용합니다.

@NotNull, @NotBlank, @Max, @Email 등을 사용하여 입력값 검증 로직을 DTO에 선언적으로 적용하고, 잘못된 데이터 유입을 @Valid를 이용해 컨트롤러단에서 즉시 차단합니다. 검증에 실패하면 서버는 400 Bad Request를 클라이언트에게 반환합니다.

Service 계층이 아닌 Controller에서 검증하는 이유는 Service에서 데이터를 처리하기 전 잘못된 데이터를 가능한 한 시스템의 가장 바깥쪽에서 처리하기 위해 Controller에서 받자마자 검증을 거칩니다. 또한 전역적 예외 처리를 통해 일관된 형식의 응답을 클라이언트에게 보내주기 용이하기 때문입니다.

Spring Boot는 예외 처리 설정을 하지 않았을 때 자동으로 BasicErrorController를 사용해 예외를 자동으로 처리합니다. 이때의 예외 처리를 사용자가 보기 편하도록 개발자가 커스텀할 수 있습니다.

@ControllerAdvice를 사용하여 예외 처리를 전역적으로 중앙화하고, @ExceptionHandler를 이용해 클라이언트에게 일관된 에러 응답을 제공합니다.

예외 처리의 우선순위는 컨트롤러 내의 @ExceptionHandler, @ControllerAdvice 내의 @ExceptionHandler, ResponseStatusException, 스프링 부트의 기본 예외 처리 순으로 처리됩니다.

Spring Boot는 애플리케이션의 상태 확인, 시스템 모니터링, 관리, 운영 자동화 등(서비스가 잘 돌아가고 있는가를 확인)을 위한 운영용 엔드 포인트를 Actuator라는 모듈로 제공합니다.

application.yml 파일에서 설정을 통해 /actuator/health, /actuator/metrics 등의 엔드포인트를 통해 애플리케이션의 운영 상태를 실시간으로 모니터링합니다.

뿐만 아니라, Spring Boot에서는 강력한 로깅 기능도 제공합니다. 애플리케이션 로그 관리가 중요한 이유는 문제 진단 및 원인 분석에 용이하고, 시스템 모니터링 및 운영 효율화 측면에서 반드시 필요합니다. Spring Boot는 로그 시스템을 자동으로 구성해줘 초기 설정만 한다면 바로 로그 관리가 가능합니다.

SLF4J는 자바 생태계에서 가장 널리 사용되는 로그 파사드(Facade) 라이브러리입니다. 직접 로그를 기록하지 않고 내부적으로 실제 로깅 구현체에 위임하는 방식을 취하고 있습니다.

여기서 로그 파사드란 여러 로깅 프레임워크를 일관된 API로 감싸서 사용할 수 있게 해주는 중간 계층을 의미합니다. 만약 Logback이라는 로그 파사드를 사용하다가 Log4j로 변경하고자 할 때, 코드 변경 없이 build.gradle 또는 pom.xml에서 의존성만 교체한다면 변경 가능합니다.

결론

지금까지 살펴본 백엔드 개발의 흐름은 단순한 코드 작성을 넘어, 객체지향 원칙이라는 단단한 기초 위에 Spring Boot라는 효율적인 도구를 얹고, JPAJWT, AOP와 같은 현대적인 기술 스택을 조화롭게 결합하는 과정입니다.

결국 백엔드 개발자에게 요구되는 역량은 특정 기술의 단순한 사용법 숙지가 아니라, "왜 이 기술이 도입되었는가?" 에 대한 본질적인 이해를 바탕으로 최적의 아키텍처를 설계하는 능력입니다. 이 글에서 다룬 핵심 개념들은 견고하고 확장 가능한 서비스를 구축하기 위한 든든한 이정표가 되어줄 것입니다.

profile
항상 왜?를 생각하는 개발자

0개의 댓글