BACKEND/SPRING

객체지향 프로그래밍과 SOLID 원칙 그리고 Spring

우진하다 2023. 7. 11. 18:18

시작하며.

스프링을 본격적으로 공부하기 전에 스프링이 도입 된 배경과
이와 관련된 객체지향 프로그래밍부터 차근차근 정리해보려 합니다.

 

객체지향 프로그래밍을 하는 이유?

대규모 서비스 개발 그리고 기능 개선과 같은 소프트웨어 개발 작업은
지속해서 변화하고 수정할 수 있어야 합니다.

이를 위해 코드의 재사용성, 유지보수성, 확장성, 가독성 등을 향상시키고
복잡한 시스템 개발을 보다 구조적이고 효율적으로 관리할 수 있는 방법이 필요합니다.

이런 부분을 해소하기 위해 다양한 프로그래밍 패러다임이 시대적으로 제시되었으며
보다 적합한 대중적인 방법 중 하나가 바로 객체지향 프로그래밍입니다. 

 

객체지향과 객체지향 프로그래밍

객체지향(Object-Oriented)은 소프트웨어 개발 패러다임으로, 현실 세계의 개념과 구조를 컴퓨터 프로그램에 반영하는 방법입니다. 객체지향은 객체라는 독립적인 개체를 중심으로 프로그램을 구성하고, 개체들 간의 상호작용을 통해 기능을 구현하는 방식입니다.

객체지향 프로그래밍은 코드를 객체 단위로 모듈화하여 재사용성을 높입니다. 
객체는 독립적으로 존재하며, 필요한 경우 다른 객체와 상호작용하여 기능을 구현합니다. 

또한 캡슐화를 통해 객체의 상태와 동작을 외부로부터 숨기고, 필요한 인터페이스를 제공합니다. 
이를 통해 코드의 유지보수성과 확장성이 향상되며, 객체 간의 결합도가 낮아져 시스템을 구조적으로 관리할 수 있습니다.

또한 상속과 다형성을 통해 코드의 재사용성과 유연성을 높이며, 코드의 가독성도 개선됩니다. 
상속을 통해 공통된 특성을 추출하고 재사용할 수 있으며, 
다형성을 활용하여 유연하고 확장 가능한 코드를 작성할 수 있습니다.

객체지향 프로그래밍은 복잡한 시스템 개발을 보다 구조적이고 효율적으로 관리할 수 있는 방법 중 하나입니다.
코드의 재사용성, 유지보수성, 확장성, 가독성 등을 향상시켜 대규모 서비스 개발과 기능 개선을 보다 효과적으로 수행할 수 있습니다.

 

객체지향 프로그래밍을 잘하는 방법 - SOLID 원칙

  • 객체 모델링 및 설계
    좋은 객체 모델링과 설계는 객체지향 프로그래밍의 핵심
    문제를 도메인 영역으로 분해하고
    적절한 객체들을 식별하고 그들 간의 관계를 설계하는 것이 중요
    클래스와 인터페이스를 적절히 추상화하여 객체의 상태와 동작을 정의하고 관리

 

  • 단일 책임 원칙(Single Responsibility Principle, SRP)
    클래스나 객체는 단 하나의 책임을 가져야 합니다.
    이를 통해 클래스를 작고 응집력 있는 단위로 유지할 수 있고
    책임을 분리함으로써 코드를 이해하기 쉽고 변경이 용이하게 합니다.
예) 학생 정보 관리 시스템

Student 클래스: 학생의 정보를 저장하고 관리하는 책임을 가짐
StudentRepository 클래스: 학생 데이터를 데이터베이스에 저장하고 조회하는 책임을 가짐

 

  • 개방 폐쇄 원칙 (Open-Closed Principle, OCP)
    소프트웨어 엔티티(클래스, 모듈, 함수 등) 확장에는 열려있어야 하고, 변경에는 닫혀있어야 합니다.
    새로운 기능을 추가하거나 수정할 때 기존의 코드를 변경하는 것이 아닌
    확장을 통해 기능을 변경하도록 설계해야 합니다.
    이를 통해 코드의 안정성을 유지하면서도 새로운 기능을 추가하거나 변경할 수 있습니다.
    즉, 수정하지 말고 클래스를 신규 추가하세요
예제)도형 계산기

Shape 클래스: 도형의 기본 속성과 추상 메서드를 정의
Circle 클래스와 Rectangle 클래스: Shape 클래스를 상속받아 각 도형의 특정 계산을 수행

 

  • 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
    자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
    부모 클래스에서 정의한 규약(계약)을 자식 클래스에서 지켜야 합니다.
    이를 통해 다형성을 구현하고, 객체의 일관성을 유지하며, 코드의 재사용성과 확장성을 향상시킵니다.
예) 동물 예제

Animal 클래스: 동물의 기본 특성과 행동을 정의
Dog 클래스와 Cat 클래스: Animal 클래스를 상속받아 각 동물의 특정 행동을 구현

 

  • 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
    클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
    즉 인터페이스는 클라이언트가 필요로 하는 기능만 제공해야 합니다.
    불필요한 의존성을 제거하고, 인터페이스의 명확성과 결합도를 낮추어
    코드의 유지보수성과 재사용성을 향상시킵니다.

예) 프린터 인터페이스

Printer 인터페이스: 인쇄(print) 메서드를 포함
Scanner 인터페이스: 스캔(scan) 메서드를 포함
Photocopier 클래스: Printer와 Scanner 인터페이스를 구현하여 인쇄와 스캔 기능을 제공

 

  • 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
    하위 모듈의 변경이 상위 모듈의 변경을 요구하는 의존성을 끊어내야 한다.
    의존성은 구체적인 구현이 아닌 추상화에 의존해야 합니다.
    즉, 고수준 모듈은 저수준 모듈에 의존하지 않고, 모두 추상화에 의존해야 합니다. 이를 통해 의존성의 역전을 구현하고, 시스템의 유연성과 확장성을 향상시킵니다.

예) 주문 처리 시스템

Order 클래스: 주문 정보를 처리하는 기능을 제공
PaymentGateway 인터페이스: 결제 처리를 추상화한 인터페이스
Order 클래스가 PaymentGateway 인터페이스에 의존하도록 설계하여 
결제 처리에 대한 구체적인 구현을 분리

SOLID 원칙은 객체지향 설계의 원칙들로서, 
객체간의 결합도를 낮추고 응집력을 높여서 유지보수성과 재사용성을 향상시키는데 도움을 줍니다. 
이 원칙들을 준수하여 설계한 코드는 변경에 유연하며 확장 가능한 구조를 갖게 되어 대규모 서비스 개발 및 기능 개선에 효과적으로 활용될 수 있습니다.

 

 SOLID 원칙을 잘 준수하여 프로그래밍 하는 방법?

객체지향적으로 SOLID 원칙을 잘 준수하면서 코드를 작성해보면..?
결국 Spring과 비슷한 기능을 만들게 됩니다.
그러니까 이미 잘 만들어진 스프링을 잘 활용하고 사용하자 라는 결론에 이르게 됩니다.

스프링 자체가 SOLID 원칙을 완전히 준수하는 것은 아닙니다. 
스프링은 SOLID 원칙을 지원하고 프로그래머가 이를 적용할 수 있도록 도와주는 도구와 기능을 제공합니다.

 

Spring과 주요 특징 및 기능.

스프링(Spring)은 자바 기반의 오픈 소스 애플리케이션 프레임워크로, 
엔터프라이즈급 애플리케이션 개발을 위한 다양한 기능과 추상화된 라이브러리를 제공합니다. 
스프링은 애플리케이션의 개발과 구성, 배포 및 유지보수를 위한 효율적인 방법을 제공하며, 
강력한 기능과 유연성을 갖추고 있습니다.

의존성 주입(Dependency Injection, DI)
스프링은 DI를 통해 클래스 간의 의존성을 완화시키고 결합도를 낮출 수 있도록 지원합니다. 
이는 DIP(의존성 역전 원칙)을 준수하기 위한 핵심 개념입니다.

제어 역전(Inversion of Control, IoC)
스프링은 IoC 컨테이너를 통해 객체의 생명주기를 관리하고 의존성을 주입해주는 제어 역전 개념을 제공합니다. 
이는 OCP(개방 폐쇄 원칙)와 DIP(의존성 역전 원칙)을 지원하기 위한 방식입니다.

관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)
스프링은 AOP를 활용하여 공통 로직을 분리하여 적용할 수 있는 기능을 제공합니다. 
이는 SRP(단일 책임 원칙)을 준수하고 중복 코드를 줄일 수 있는 방법 중 하나입니다.

인터페이스와 추상화
스프링은 인터페이스와 추상화를 활용하여 강력한 다형성을 제공합니다. 
이는 LSP(리스코프 치환 원칙)과 ISP(인터페이스 분리 원칙)를 준수하기 위한 방법 중 하나입니다.

스프링은 SOLID 원칙과 OOP 원칙을 이해하고 적용할 수 있는 유용한 도구를 제공하며, 
개발자가 이를 활용하여 원칙을 준수하고 유지보수 가능하고 확장성이 높은 소프트웨어를 개발할 수 있도록 돕습니다. 
하지만 스프링을 사용하더라도 원칙을 준수하는 것은 개발자의 책임이며, 
올바른 설계와 구현을 위해 원칙을 이해하고 적용해야 합니다.

 

SOLID 원칙을 준수하면서 스프링을 활용하여 프로그래밍하는 방법

단일 책임 원칙 (SRP)
스프링의 DI(Dependency Injection) 기능을 활용하여 클래스 간의 의존성을 낮춥니다. 
이를 통해 클래스는 하나의 책임에 집중하고, 의존성을 주입받아 다른 클래스와의 협력을 통해 기능을 완성합니다.

스프링의 AOP(Aspect-Oriented Programming)를 활용하여 공통 로직을 분리하여 적용합니다. 
이를 통해 각 클래스는 자신의 핵심 로직에만 집중하고, 공통 로직은 별도의 관심사로 분리합니다.

회원 가입 기능을 가진 웹 애플리케이션을 개발한다고 가정해봅시다.
MemberService 클래스: 회원 가입 로직을 담당하는 클래스로, 
회원 정보의 유효성 검사, 중복 체크 등 회원 가입에 관련된 책임을 갖습니다.

MemberController 클래스: 웹 요청을 받고, MemberService를 호출하여 
회원 가입을 처리하는 역할을 수행합니다.
이렇게 역할을 분리하면 각 클래스는 단일 책임을 가지게 됩니다.

개방 폐쇄 원칙 (OCP)
스프링의 IoC(Inversion of Control) 컨테이너를 활용하여 확장 가능한 코드를 작성합니다. 
인터페이스와 추상화를 활용하여 구체적인 구현에 의존하지 않고, 
인터페이스를 통해 다양한 구현체를 주입받아 사용합니다. 
이를 통해 새로운 기능을 추가하거나 변경할 때 코드의 수정 없이 확장할 수 있습니다.

게시판 애플리케이션에서 게시글 저장 기능을 개발한다고 가정해봅시다.

PostService 인터페이스: 게시글 저장 기능을 추상화한 인터페이스입니다.

JpaPostRepository 클래스: 게시글을 데이터베이스에 저장하는 구체적인 구현체입니다.

PostService의 메서드에는 JpaPostRepository를 주입받아 게시글 저장 기능을 수행합니다. 
이렇게 하면 PostService는 인터페이스에 의존하면서, 
실제 구현체에는 의존하지 않으므로 확장성과 유연성이 높아집니다. 

새로운 저장 방식이 필요하다면 새로운 구현체를 만들어 주입하면 됩니다.

리스코프 치환 원칙 (LSP)
스프링의 다형성과 인터페이스를 활용하여 LSP를 지킬 수 있습니다. 
상속과 인터페이스를 활용하여 다양한 타입을 처리하는 기능을 구현하고, 
이를 통해 자식 클래스가 부모 클래스를 대체할 수 있도록 설계합니다.

도형 계산기 애플리케이션을 개발한다고 가정해봅시다.

Shape 인터페이스: 도형의 기본 메서드를 정의한 인터페이스입니다.

Circle 클래스와 Rectangle 클래스: Shape 인터페이스를 구현한 구체적인 클래스로, 
각 도형의 특성과 계산 메서드를 구현합니다. 
이 때, Circle 클래스와 Rectangle 클래스는 Shape 인터페이스를 대체할 수 있어야 합니다.

인터페이스 분리 원칙 (ISP)
스프링의 DI와 인터페이스를 적절하게 활용하여 ISP를 준수합니다. 
클래스가 자신이 사용하지 않는 인터페이스에 의존하지 않도록 
인터페이스를 세분화하여 필요한 기능만 포함하도록 설계합니다. 
이를 통해 의존성을 최소화하고, 불필요한 의존성을 제거합니다.

사용자 인증 기능을 개발한다고 가정해봅시다.

AuthService 인터페이스: 사용자 인증과 관련된 기능을 정의한 인터페이스입니다.

LoginService 인터페이스와 RegisterService 인터페이스: AuthService 인터페이스를 상속받아 
로그인과 회원 가입에 관련된 기능을 분리합니다. 

이렇게 함으로써 클라이언트는 자신이 필요한 기능만 사용할 수 있으며, 
불필요한 의존성을 피할 수 있습니다.

의존성 역전 원칙 (DIP)
스프링의 DI 컨테이너를 활용하여 DIP를 준수합니다. 
의존성을 주입받는 형태로 객체를 생성하고, 구체적인 구현체에 의존하는 것이 아니라 
인터페이스나 추상화에 의존하도록 설계합니다. 
이를 통해 객체 간의 결합도를 낮추고, 유연한 코드를 작성할 수 있습니다.

주문 처리 애플리케이션을 개발한다고 가정해봅시다.

OrderService 클래스: 주문 처리 로직을 담당하는 클래스입니다.

PaymentGateway 인터페이스: 결제 처리를 추상화한 인터페이스입니다.

OrderService는 PaymentGateway 인터페이스에 의존하도록 설계하여 
구체적인 결제 처리 로직을 분리하고, 의존성을 역전시킵니다. 

이를 통해 OrderService는 실제 결제 처리 로직의 변경 없이 
PaymentGateway의 구현체를 교체할 수 있습니다.