[Spring] Spring, 왜 좋을까?

김태환·2024년 12월 14일

Spring을 사용하면 개발 생산성을 높이고 유지보수를 쉽게 만들어준다고 한다.

개발을 하다 보면 왜 그런지 항상 느끼지만 말로 정리를 못하겠어서 여기에 한 번 정리해야겠다.

Spring Framework는 자바 기반 엔터프라이즈 애플리케이션 개발을 지원하는 플랫폼이다.

Spring의 특징

1. POJO (Plain Old Java Object, 뽀죠)

Spring이 POJO 프로그래밍을 추구한다고 한다. Spring의 가장 기본적인 설계 철학이라고 할 정도이다.

POJO는 특정 프레임워크나 라이브러리에 종속되지 않은 순수한 Java 객체를 일컫는 말이다.

Spring은 POJO 프로그래밍을 위해 IoC/DI, AOP, PSA를 지원한다고 한다.

장점:

  • 프레임워크 종속성 제거 (비침투성 보장)
    Spring 프레임워크에 종속되지 않아 다른 프레임워크, 환경으로 쉽게 전환이 가능하다
  • 간결한 설계
    불필요한 설정 & 복잡한 설정 Bye~! 객체지향 프로그래밍 원칙도 유지해준다

예시:

  • POJO 객체 사용:
    Spring 설정이 없어도 해당 코드를 사용할 수 있다.

    public class UserService {
        public String getUser() {
            return "User Info";
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            UserService userService = new UserService();
            System.out.println(userService.getUser());
        }
    }

2. IoC (Inversion of Control / 제어의 역전) & DI (Dependency Injection / 의존성 주입)

IoC (Inversion of Control)
객체의 생성과 의존성 관리를 개발자가 아니라 Spring IoC 컨테이너(이하 Spring 컨테이너)가 맡는 것이다. 필요한 의존성을 주입하는 과정을 프레임워크에 위임하여 내가 관리하지 않으면서 코드의 결합도를 낮추고 유연성을 높이는 것이다.

장점:

  • 객체 간 의존성을 명시적으로 관리하여 코드의 유지보수성과 테스트 용이성을 향상시킨다
  • 애플리케이션 구조가 더 모듈화되어 다양한 환경에서 쉽게 확장할 수 있다

예시:

  • IoC 적용
    이 코드에서는 OrderService가 특정 PaymentService의 의존하게 된다. PaymentService를 다른 구현체로 교체하려면 OrderService의 코드까지 수정해야 한다. 직접 의존성을 생성하게 되면 PaymentService에 대한 결합도로 인해 재사용성도 떨어진다.
    OrderService를 테스트 시에도 PaymentService도 함께 초기화해야한다.

    class OrderService {
        private PaymentService paymentService;
    
        public OrderService() {
            // 객체를 직접 생성 (강한 결합)
            this.paymentService = new PaymentService();
        }
    
        public void placeOrder() {
            paymentService.processPayment();
        }
    }
    
    class PaymentService {
        public void processPayment() {
            System.out.println("Payment processed.");
        }
    }
  • IoC 적용
    이 코드에서는 객체 생성 책임을 Spring 컨테이너에 위임한다. 이로써 OrderService는 PaymentService의 특정 구현체에 대한 결합도가 낮아지고, 해당 구현체를 쉽게 변경하고 확장할 수 있다.
    OrderService를 테스트 시에 PaymentService를 초기화하지 않고 Mock 객체로 대체할 수 있어 독립적인 테스트가 가능하다.

    @Service
    class PaymentService {
        public void processPayment() {
            System.out.println("Payment processed.");
        }
    }
    
    @Service
    class OrderService {
        private final PaymentService paymentService;
    
        // 의존성 주입 (생성자 주입)
        public OrderService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }
    
        public void placeOrder() {
            paymentService.processPayment();
        }
    }

DI (Dependency Injection)
DI는 IoC를 구현하는 방법 중 하나이다. 객체가 필요로 하는 의존성을 외부(Spring 컨테이너)에서 주입 받는 방식이다.

Spring은 애너테이션 기반, Java Config 클래스, XML 설정 등으로 DI를 지원한다.

동작 원리:

  • 객체 간의 관계를 Spring 컨테이너가 설정한다
  • @Autowired, 생성자 주입 등의 방식을 통해 의존성을 주입한다

장점:

  • 의존성 주입을 통해 객체 간 결합도를 낮춘다
  • Mock 객체를 쉽게 주입할 수 있어 테스트 또한 쉬워진다

예시:

  • 생성자 주입(요즘 권고된다):

    @Service
    class OrderService {
        private final PaymentService paymentService;
    
        @Autowired // 생성자 주입 하나일 때는 Autowired 생략 가능
        public OrderService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }
    }
  • Setter 주입:

    @Service
    class OrderService {
        private PaymentService paymentService;
    
        @Autowired
        public void setPaymentService(PaymentService paymentService) {
            this.paymentService = paymentService;
        }
    }
  • Field 주입:

    @Service
    class OrderService {
        @Autowired
        private PaymentService paymentService;
    }

@Component, @Controller, @Service, @Repository을 사용하면 @ComponentScan으로 자동적으로 해당 객체들을 Bean으로 Spring 컨테이너에 등록해준다.

반면, 명시적으로 Bean을 등록하고자 할 때에는 @Configuration, @Bean을 사용하면 된다.

  • 명시적 Bean 등록

    @Configuration
    public class AppConfig {
    
        @Bean
        public PaymentService paymentService() {
            return new PaymentService(); // 빈으로 등록
        }
    
        @Bean
        public OrderService orderService() {
            return new OrderService(paymentService()); // 의존성 주입
        }
    }

3. PSA (Portable Service Abstraction / 일관된 서비스 추상화)

PSA는 Spring이 제공하는 추상화 원칙으로, 특정 기술과 환경에 종속되지 않도록 제공하는 것이다.

PSA를 통해 개발자는 다양항 기술 스택에서 동일한 방식으로 코드를 작성할 수 있고, Spring 하위 구현의 세부사항을 몰라도 서비스를 개발할 수 있다!

장점:

  • 다양한 기술 통합
    JDBC, JPA, Hibernate 등 다양한 데이터 접근 기술과 쉽게 통합된다
  • 코드의 이식성
    기술 스택, 실행 환경이 바뀌어도 Spring의 추상화 계층 덕에 코드 수정이 없다
    팀장님이 MySQL에서 PostgreSQL로 바꾸시래😭 → Spring의 PSA 특징 덕분에 코드 수정할 게 없넹! (광고 같죠?)
  • 일관된 프로그래밍 모델
    개발하면서 기술 세부 사항 공부하고 바쁘고 그런 거 없이 비즈니스 로직에만 집중할 수 있다고 한다는데, 이건 잘 모르겠다. 기술 세부 사항 모르고 쓰는 개발자는 못 봐서...

구현체 교체도 쉽게 할 수 있고, 다른 기술(SQL 같은)의 종속성도 낮아지고, 이로써 환경 변경의 유연성을 가지게 된다.

예시:

  • JPA 사용 시:
    데이터베이스를 변경하게 되어도, ID를 기반으로 회원을 조회하는 코드는 변하지 않는다.
    public interface UserRepository extends JpaRepository<User, Integer> {
        // 메서드 정의만으로 데이터베이스 작업 가능
        User findById(int id);
    }

4. AOP (Aspect-Oriented Programming / 관점 지향 프로그래밍)

Spring은 공통적으로 사용되는 로직(로깅, 트랜잭션 관리 등)을 핵심 비즈니스 로직과 분리하여 모듈화할 수 있도록 지원한다.

장점:

  • 코드 중복을 줄이고, 비즈니스 로직을 더 명확하게 유지할 수 있다
  • 유지보수와 확장성이 향상된다

예시:

  • 로그를 출력해주는 AOP 클래스 적용 예시:
    @EnableAspectJAutoProxy 추가해주고

    // 비즈니스 로직 클래스
    @Service
    public class PaymentService {
    
        public void processPayment() {
            System.out.println("Processing payment...");
            // 결제 로직
        }
    
        public void cancelPayment() {
            System.out.println("Cancelling payment...");
            // 결제 취소 로직
        }
    }
    // 로그 전용 Aspect 클래스
    @Aspect
    @Component
    @Slf4j
    public class LoggingAspect {
    
        // Pointcut: PaymentService 클래스의 모든 메서드
        @Pointcut("execution(* com.example.app.PaymentService.*(..))")
        public void allPaymentMethods() {}
    
        // Advice: 메서드 실행 전 로그 출력
        @Before("allPaymentMethods()")
        public void logBefore() {
            logger.info("[AOP] Method execution started");
        }
    
        // Advice: 메서드 실행 후 로그 출력
        @After("allPaymentMethods()")
        public void logAfter() {
            logger.info("[AOP] Method execution finished");
        }
    
        // Advice: 메서드 정상 완료 후 로그 출력
        @AfterReturning("allPaymentMethods()")
        public void logAfterReturning() {
            logger.info("[AOP] Method executed successfully");
        }
    }
    // 이 코드가 수행되면
    paymentService.processPayment();
    paymentService.cancelPayment();
    # 이렇게 로그가 출력(저장)될 것이다
    INFO - [AOP] Method execution started
    Processing payment...
    INFO - [AOP] Method execution finished
    INFO - [AOP] Method executed successfully
    INFO - [AOP] Method execution started
    Cancelling payment...
    INFO - [AOP] Method execution finished
    INFO - [AOP] Method executed successfully

Spring Framework? Spring Boot? 둘이 뭐가 달라???

Spring Framework

Spring Framework는 엔터프라이즈 애플리케이션 개발을 위한 프레임워크입니다.

특징:

  • 수동 설정 필요:
    XML, Java Config 등을 사용하여 빈(Bean)과 의존성을 직접 설정해야 합니다
    데이터베이스, 웹 서버, 트랜잭션 관리, DispatcherServlet 등도 수동으로 구성해야 한다
  • 직접 라이브러리 관리:
    프로젝트 시작 시 필요한 라이브러리를 직접 추가하고, 버전을 관리해야 한다
  • 외부 서버 사용:
    내장 서버가 없어서 애플리케이션 실행을 위해 Tomcat, Jetty 같은 외부 서버를 별도로 설정해야 하다
  • 유연한 커스터마이징:
    이렇게 어려운데 왜 😫 → 세부 제어를 필요로 하는 등 다양한 환경, 요구사항에 맞춰 커스터마이징이 가능하다고 한다

Spring Framework는 설정 과정이 어렵고 시간이 오래 걸리지만, 세밀한 제어가 필요한 복잡한 애플리케이션에 적합하다.

Spring Boot

Spring Boot는 Spring 기반 애플리케이션을 빠르게 개발할 수 있도록 도와주는 프레임워크이다.

특징:

  • 자동 설정:
    @SpringBootApplication, @Transactional 등으로 다양한 설정을 자동으로 처리한다
    application.properties 또는 application.yml 파일로 간단히 애플리케이션 환경을 구성할 수 있다
  • 내장 서버 제공:
    Tomcat, Jetty와 같은 내장 서버를 기본으로 제공하므로 별도의 서버 설정 없이 애플리케이션을 실행할 수 있다
  • Starter 패키지:
    spring-boot-starter-* 의존성을 사용하여 필요한 라이브러리를 쉽게 추가할 수 있다
    프로젝트 초기 설정이 간편하다
  • 빠른 시작:
    개발자가 설정에 시간을 들이지 않고 비즈니스 로직 구현에 집중할 수 있다

Spring Boot는 초기 설정을 간소화하고, 개발 생산성을 높이는 데 최적화된 프레임워크이다.

결론

Spring은 POJO를 추구하고, 이를 실현시키기 위해 IoC/DI, PSA, AOP를 지원한다.

IoC/DI는 객체의 생성과 의존성 관리를 Spring IoC 컨테이너에게 위임하여 코드 간의 결합도를 낮추고 유연성을 높이는 것이다.

PSA는 잘 만든 인터페이스를 만들어서 코드의 수정을 최소화할 수 있도록 하는 것이다.

AOP는 여러 비즈니스 로직 간 공통적인 로직을 모듈화 하는 것이다.

Spring Framework와 Spring Boot의 차이점은 아래 표로 간략히 정리해봤다.

항목Spring FrameworkSpring Boot
설정 방식수동 설정 필요 (XML, Java Config)자동 설정 제공 (기본 설정 자동 처리)
라이브러리 관리직접 추가 및 버전 관리Starter 패키지 제공
서버 구성외부 서버 필요내장 서버 제공 (Tomcat, Jetty 등)
개발 속도설정 시간이 오래 걸림빠른 프로젝트 시작 가능
유연성복잡한 요구사항을 세밀하게 커스터마이징 가능기본 제공되는 설정으로 빠르게 개발 가능



참고 및 출처

https://www.elancer.co.kr/blog/detail/158

https://www.codestates.com/blog/content/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

https://memodayoungee.tistory.com/102

https://khj93.tistory.com/entry/Spring-Spring-Framework%EB%9E%80-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-%ED%95%B5%EC%8B%AC-%EC%A0%95%EB%A6%AC#google_vignette

profile
이로운 개발자

0개의 댓글