본문 바로가기
개발일지/Spring

Spring, 좋은 객체 지향 프로그래밍이란?, SOLID, 객체 지향 설계와 스프링 (by.김영한)

by 꾸주니=^= 2025. 1. 21.

Spring

1. Spring?

spring 공식 사이트

- 스프링은 자바 언어 기반의 프레임워크
- 자바 언어의 가장 큰 특징 - 객체 지향 언어
- 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
- 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

로드 존슨이 EJB 불편함을 극복하기 위해 만든 프레임워크입니다.

 

 

2. 스프링의 주요 목표

- 경량화: 필요 없는 기능을 최소화하여 가볍게 동작
- POJO 기반 개발: 일반적인 자바 객체(Plain Old Java Object)를 기반으로 개발
- DI(의존성 주입): 객체 간 의존성을 설정 파일이나 주석(annotation)을 통해 관리
- AOP(관점 지향 프로그래밍): 로깅, 보안 등과 같은 부가 기능을 쉽게 추가
- 모듈화: 필요에 따라 원하는 기능만 사용할 수 있는 구조

 

3. 스프링의 주요 특징

스프링은 다양한 특징을 통해 개발자가 생산성을 높이고 효율적으로 코드를 작성할 수 있도록 돕습니다.

 


3.1. DI (Dependency Injection) - 의존성 주입

- 객체 간의 의존 관계를 코드가 아닌 설정으로 관리합니다.
- 설정 파일(XML 또는 Java Config)이나 어노테이션(@Autowired)을 사용하여 객체 간의 결합을 줄입니다.

@Component
public class Car {
    private Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}

 

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

핵심 비즈니스 로직과 부가 기능(로깅, 보안, 트랜잭션)을 분리하여 관리합니다.
Advice(어드바이스)와 Pointcut(포인트컷)을 통해 코드의 중복을 줄입니다.

3.3. 트랜잭션 관리

데이터베이스 트랜잭션을 쉽게 관리할 수 있도록 지원합니다.
@Transactional 어노테이션을 통해 코드에 최소한의 설정만으로 트랜잭션을 처리할 수 있습니다.

3.4. 테스트 용이성

테스트 코드 작성을 쉽게 할 수 있도록 DI를 기반으로 유닛 테스트를 지원합니다.
Mock 객체를 쉽게 사용할 수 있습니다.

3.5. 모듈화와 확장성

스프링은 여러 모듈로 구성되어 있습니다. 원하는 기능만 골라 사용할 수 있습니다.


 

4. 스프링의 주요 구성 요소

스프링은 여러 모듈로 나뉘어 있으며, 각 모듈은 특정 기능을 담당합니다.

 


4.1. 스프링 코어(Core)

- DI와 IoC(Inversion of Control) 컨테이너를 제공
- 객체 생성 및 의존성 관리를 담당

4.2. 스프링 AOP

- 관점 지향 프로그래밍 기능을 지원

4.3. 스프링 데이터(Spring Data)

- 데이터 접근 계층을 쉽게 구현할 수 있는 도구 제공
- JPA, MongoDB, Redis 등과의 통합 지원

4.4. 스프링 웹(Spring Web)

- 웹 애플리케이션 개발에 필요한 기능 제공
- HTTP 요청/응답 처리 및 MVC 패턴을 지원

4.5. 스프링 부트(Spring Boot)

- 스프링 애플리케이션을 더 간단하고 빠르게 개발할 수 있는 도구
- 내장 웹 서버(Tomcat, Jetty 등)와 설정 자동화 기능 제공


 

5. 스프링 부트 (Spring Boot)

스프링 부트는 스프링 프레임워크를 더 쉽고 간단하게 사용할 수 있도록 만든 확장 프레임워크입니다.

 

스프링 부트의 장점

- 내장된 웹 서버 제공 (Tomcat, Jetty 등)
- 복잡한 설정 없이 기본값으로 빠르게 실행 가능
- Spring Initializr (스프링 스타터)을 통해 프로젝트 초기 설정을 간소화

@SpringBootApplication
public class SpringBootExample {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootExample.class, args);
    }
}

 

6. 스프링 MVC 패턴

스프링은 MVC(Model-View-Controller) 아키텍처를 지원하여 웹 애플리케이션 개발을 쉽게 만듭니다.

- Model : 데이터를 관리하는 계층 (Service, DAO 등)
- View : 사용자에게 보여지는 화면 (HTML, JSP 등)
- Controller : 요청을 처리하고 적절한 응답을 생성

@Controller
public class MyController {

    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello, Spring!");
        return "hello"; // View 이름
    }
}

 


 

좋은 객체 지향 프로그래밍이란?

1. 객체 지향 특징

- 추상화
- 캡슐화
- 상속
- 다형성

 

2. 객체 지향 프로그래밍

- 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위,
    즉 "객체"들의 모임으로 파악하고자 합니다.
- 각각의 객체는 메시지를 주고 받고, 데이터를 처리할 수 있습니다.
- 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.
    한마디로, 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있습니다.

 

3. 다형성의 실세계 비유 예시

- 운전자 - 자동차
- 공연 무대
- 키보드, 마우스, 세상의 표준 인터페이스들
- 정렬 알고리즘
- 할인 정책 로직

 

4. 역할과 구현을 분리

역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해집니다.

- 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있음
- 유연하고 변경이 용이
- 확장 가능한 설계
- 클라이언트에 영향을 주지 않는 변경 가능
- 인터페이스를 안정적으로 잘 설계하는 것이 중요

 

자바 언어

자바 언어의 다형성을 활용
    > 역할 = 인터페이스
    > 구현 = 인퍼테이스를 구현한 클래스, 구현 객체

객체를 설계할 때 역할과 구현을 명확히 분리
객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기

 

5. 다형성의 본질

- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다.
- 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작해야합니다.
- 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있습니다. 

 

6. 스프링과 객체 지향

- 다형성이 가장 중요합니다.
- 스프링은 다형성을 극대화해서 이용할 수 있게 도와줍니다.
- 스프링에서 이야기하는 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원합니다.
- 스프링을 사용하면 구현을 편리하게 변경할 수 있습니다.

 


좋은 객체 지향 설계의 5가지 원칙(SOLID)

1. SOLID

- SRP : 단일 책임 원칙 (single responsibility principle)
- OCP : 개방-폐쇄 원칙 (Open/closed principle)
- LSP : 리스코프 치환 원칙 (Liskov substitution principle)
- ISP : 인터페이스 분리 원칙 (Interface segregation principle)
- DIP : 의존관계 역전 원칙 (Dependency inversion principle)

 

1.1 SRP 단일 책임 원칙

- 한 클래스는 하나의 책임만 가져야 합니다.
- 하나의 책임이라는 것은 모호합니다.
    - 클 수 있고, 작을 수 있습니다.
    - 문맥과 상황에 따라 다릅니다.
- 중요한 기준은 변경입니다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것입니다.

1.2 OCP 개방-폐쇄 원칙

- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 합니다.
- 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구협합니다.

1.3 LSP 리스코프 치환 원칙

- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 합니다.
- 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 합니다.
- 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요합니다.
- 단순히 컴파일에 성공하는 것을 넘어서는 이야기

1.4 ISP 인터페이스 분리 원칙

- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫습니다.
- 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
- 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
- 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않습니다.
- 인터페이스가 명확해지고, 대체 가능성이 높아집니다.

1.5 DIP 의존관계 역전 원칙

- 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나입니다.
- 쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
- 역할에 의존하게 해야 한다는 것과 같습니다. 
   객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있습니다.
   구현체에 의존하게 되면 변경이 아주 어려워집니다.

 


정리

- 객체 지향의 핵심은 다형성 !!
- 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없습니다.
- 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경됩니다.
- 다형성 만으로는 OCP, DIP를 지킬 수 없습니다.

 


객체 지향 설계와 스프링

스프링 이야기에 왜 객체 지향 이야기가 나올까?

스프링(Spring) 프레임워크를 이해하다 보면 객체 지향 프로그래밍(OOP)에 대한 이야기가 빠지지 않고 등장합니다.
그 이유는 스프링이 객체 지향 설계의 원칙을 기반으로 설계되었기 때문입니다.

객체 지향의 강점을 활용하여 개발자가 더 유연하고 확장 가능한 코드를 작성할 수 있도록 돕는 것이 스프링의 주요 목표 중 하나입니다.

아래에서 스프링과 객체 지향 설계가 어떻게 연결되는지, 그리고 스프링이 이를 어떻게 지원하는지 자세히 설명하겠습니다.

 

1. 객체 지향 설계의 핵심: OCP와 DIP

객체 지향 설계의 주요 원칙 중 Open-Closed Principle(OCP, 개방-폐쇄 원칙)과 Dependency Inversion Principle(DIP, 의존성 역전 원칙)은 스프링이 지향하는 설계 철학과 깊이 연관되어 있습니다.

 

OCP (Open-Closed Principle)

  • 정의: 소프트웨어 구성 요소는 "확장에는 열려 있고, 변경에는 닫혀 있어야 한다."
  • 설명: 기존 코드를 수정하지 않고도 애플리케이션의 기능을 확장할 수 있어야 합니다.
  • 스프링과의 연결: 스프링은 다형성과 의존성 주입(DI)을 활용해 OCP를 실현합니다. 새로운 기능을 추가하거나 기존 기능을 변경할 때 클라이언트 코드를 수정하지 않아도 되는 구조를 제공합니다.

 

DIP (Dependency Inversion Principle)

  • 정의: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
  • 설명: 구현체가 아닌 인터페이스에 의존함으로써 코드의 결합도를 낮추고 유연성을 높이는 원칙입니다.
  • 스프링과의 연결: 스프링은 DI(Dependency Injection)를 통해 DIP를 자연스럽게 구현할 수 있도록 돕습니다. 구체적인 구현 클래스가 아닌 추상화된 인터페이스를 기반으로 의존성을 주입받도록 설계할 수 있습니다.

 

2. 스프링의 기술: 다형성과 OCP, DIP의 실현

스프링은 아래와 같은 기술을 통해 객체 지향 설계 원칙을 효과적으로 지원합니다.

 


2.1 DI(Dependency Injection): 의존성 주입

DI는 객체 간의 의존 관계를 코드가 아닌 설정을 통해 외부에서 주입하는 방식입니다. 이를 통해 다음과 같은 효과를 얻을 수 있습니다

- 결합도 감소: 객체가 스스로 의존 객체를 생성하지 않으므로, 구현체를 변경해도 클라이언트 코드를 수정할 필요가 없습니다.
- 유연한 확장: 새로운 구현체를 쉽게 교체하거나 추가할 수 있습니다.

 

예를 들어,DI 없이 객체를 생성하면 다음과 같습니다

public class OrderService {
    private PaymentService paymentService = new CreditCardPaymentService();
}

위 코드는 CreditCardPaymentService에 강하게 결합되어 있기 때문에 다른 결제 방식을 추가하려면 코드를 수정해야 합니다.

DI를 사용하면 다음과 같이 작성할 수 있습니다.

@Component
public class OrderService {
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

위 코드에서는 PaymentService 인터페이스를 사용하므로, 구현체가 무엇인지 상관없이 동작합니다. 이를 통해 OCP와 DIP를 실현할 수 있습니다.

 

2.2 DI 컨테이너 제공

스프링은 DI 컨테이너(ApplicationContext)를 통해 객체의 생성과 의존성 주입을 관리합니다. 개발자는 객체 생성과 주입을 컨테이너에 위임함으로써 복잡한 객체 관계를 손쉽게 관리할 수 있습니다.

 

2.3 쉽게 부품을 교체하듯이 개발

- 유연한 구조: DI와 스프링 컨테이너를 통해 구현체를 간단히 교체하거나 추가할 수 있습니다.
- 테스트 용이성: Mock 객체를 주입하여 유닛 테스트를 쉽게 작성할 수 있습니다.

 


정리

- OCP와 DIP를 통해 확장 가능하고 유지 보수가 쉬운 코드를 작성하도록 돕습니다.
-
DI와 AOP 같은 기술로 객체 지향의 강점을 극대화합니다.
-
클라이언트 코드의 변경 없이 새로운 기능을 추가하거나 기존 기능을 대체할 수 있는 유연성을 제공합니다.

 


전체 요약

스프링 프레임워크는 자바의 객체 지향적 특성을 최대한 활용하여 유연하고 확장 가능한 애플리케이션 개발을 가능하게 합니다.
DI와 AOP와 같은 핵심 기능을 통해 SOLID 원칙, 특히 개방-폐쇄 원칙(OCP)과 의존관계 역전 원칙(DIP)을 자연스럽게 구현할 수 있어 코드의 유지 보수성과 재사용성을 크게 향상시킵니다.
스프링 부트와 같은 도구들은 개발 과정을 더욱 단순화하고 효율적으로 만들어, 개발자가 비즈니스 로직에 집중할 수 있도록 돕습니다.
객체 지향 설계의 강점을 바탕으로 스프링을 활용하면, 변화하는 요구사항에도 유연하게 대응할 수 있는 견고한 애플리케이션을 구축할 수 있습니다.