Java/Spring

Object-Oriented Design and Spring

WebDevLee 2024. 11. 19. 03:16

스프링 프레임워크의 기본 개념과 객체 지향 설계를 연관지어 정리하였습니다.

 

 


스프링의 탄생 배경 >

2000년대 초반, 자바 진영에서는 EJB(Enterprise Java Beans)가 표준 기술로 자리 잡고 있었으나, 복잡성과 느린 성능, 어려운 사용성 등으로 많은 개발자들에게 비판을 받게 되었습니다. 이러한 상황에서 POJO(Plain Old Java Object)라는 개념이 주목받으며, 원래의 간단한 자바 객체로 돌아가자는 움직임이 일어나게 됩니다.

 

로드 존슨이라는 개발자는 EJB의 이러한 문제점을 지적하며, EJB 없이도 높은 품질의 확장 가능한 애플리케이션을 개발할 수 있음을 보여주고자 2002년에 책을 출간했습니다. 이 책에는 30,000라인 이상의 예제 코드가 포함되어 있었으며, 이들 코드에는 스프링의 핵심 개념들이 담겨 있었습니다. 주요 개념으로는 BeanFactory, ApplicationContext, 제어의 역전(IoC), 의존관계 주입(DI) 등이 있습니다.

 

유겐 휠러와 얀 카로프는 로드 존슨에게 오픈소스 프로젝트를 제안하게 되었고, 이로써 스프링 프레임워크가 탄생하게 되었습니다. "스프링"이라는 이름은 전통적인 EJB라는 겨울을 지나 새로운 시작을 의미합니다.

 

현재 스프링 프레임워크는 단순한 자바 객체를 활용하여 복잡한 엔터프라이즈 애플리케이션을 쉽게 개발할 수 있도록 지원하며, 다양한 모듈과 생태계를 형성하고 있습니다. 특히, Hibernate를 비롯한 여러 구현체를 표준화하는 JPA의 등장으로, EJB의 엔티티 빈은 더 이상 중심적인 역할을 하지 않게 되었습니다.

이러한 변화의 흐름은 스프링 부트(Spring Boot)와 같은 프로젝트를 통해 더욱 확장되었으며, 스프링은 자바 생태계에서 여전히 중요한 위치를 차지하고 있습니다.

 

 


스프링이란? >

스프링은 다양한 기술의 모음으로 구성되어 있습니다. 핵심인 스프링(Spring) 프레임워크와 함께, 여러 스프링 기술을 편리하게 사용할 수 있도록 돕는 스프링 부트(Spring Boot)가 있습니다. 또한, 스프링 데이터, 스프링 세션, 스프링 시큐리티, 스프링 REST Docs, 스프링 배치, 스프링 클라우드 등 다양한 모듈이 함께 제공됩니다.

 

  • 스프링 데이터: 관계형 데이터베이스는 물론 NoSQL, MongoDB, Redis와 같은 데이터 저장소에서 CRUD 작업을 편리하게 수행할 수 있도록 도와줍니다. 특히, 스프링 데이터 JPA가 많이 사용됩니다.
  • 스프링 세션: 세션 관리를 간편하게 지원합니다.
  • 스프링 시큐리티: 보안 관련 기능을 쉽게 구현할 수 있도록 지원합니다.
  • 스프링 REST Docs: API 문서화를 효율적으로 돕습니다.
  • 스프링 배치: 배치 처리에 특화된 기능을 제공합니다.
  • 스프링 클라우드: 클라우드 환경에 최적화된 기술을 제공합니다.

이 외에도 다양한 스프링 기술들이 있습니다. 더 많은 프로젝트를 확인하려면 spring.io 사이트의 "Projects > Overview"를 방문해 보세요.

 

 


< Spring Framework >

스프링 프레임워크는 아래와 같이 다양한 기술로 구성되어 있습니다.

 

  • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트 등.
  • 웹 기술: 스프링 MVC, 스프링 WebFlux.
  • 데이터 접근: 트랜잭션, JDBC, ORM 지원, XML 지원.
  • 기술 통합: 캐시, 이메일, 원격 접근, 스케줄링.
  • 테스트: 스프링 기반 테스트 지원.
  • 언어: 코틀린, 그루비.

해당 포스팅에서는 주로 스프링의 핵심 기술에 초점을 맞춥니다.

 

 


< Spring Boot >

위와 같은 스프링의 모든 기술들을 편리하게 사용할 수 있도록 도와주는것이 Spring Boot입니다.
(요즘은 기본적으로 사용하는 추세)

스프링 부트는 스프링 프레임워크와 별개로 사용되지 않습니다. 스프링 부트는 스프링 프레임워크를 기반으로 하여 다른 기술들을 편리하게 사용할 수 있도록 해줍니다.

 

[ 장점 ]

  1. 단독 애플리케이션: 단독으로 실행 가능한 스프링 애플리케이션 생성이 쉽습니다.
  2. 내장 서버: Tomcat 같은 웹 서버를 내장하여 별도 설치가 필요 없습니다.
    - 과거에는 스프링으로 웹 애플리케이션을 만들 때 Tomcat 설치 및 설정이 필요했습니다.
  3. 스타터 종속성: 하나의 라이브러리만 받아도 관련 의존하는 라이브러리 모두가 다운 받아집니다.
  4. 자동 구성: 스프링 버전과 외부 라이브러리를 호환되게 자동 설정해줍니다.
  5. 프로덕션 준비 기능: 메트릭, 상태 확인, 외부 구성 등의 기능을 제공합니다.
  6. 간결한 설정: 스프링 프레임워크만 사용할 때의 복잡한 설정을 간소화합니다.

 

 


< 스프링의 핵심 >

📌 스프링의 핵심은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크라는 점입니다.
스프링은 웹 서버를 자동으로 띄우거나 전자정부 프레임워크 때문에 사용하는 것이 아닙니다. 그런 기능들은 단지 제공되는 여러 기능 중 일부에 지나지 않습니다. 과거 EJB를 사용하면 지저분하고 의존적인 개발 방식으로 인해 객체 지향 언어인 Java의 장점을 충분히 활용할 수 없었습니다. 이를 해결하기 위해 스프링은 객체 지향의 강력한 특징을 극대화하여 개발자가 보다 효율적이고 깔끔한 애플리케이션을 설계할 수 있도록 돕는 것을 주요 목표로 삼고 있습니다. 그 결과, 스프링을 통해 웹 애플리케이션 제작이나 클라우드, 마이크로서비스 환경에서의 애플리케이션 구성이 더 용이해집니다.

 

 


< 객체 지향 프로그래밍이란? >

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 데이터를 객체로 관리하고, 이 객체가 서로 상호 작용하는 방식으로 프로그램을 설계하는 패러다임입니다. 객체 지향의 주요 특징에는 캡슐화(Encapsulation), 상속(Inheritance), 다형성(Polymorphism), 추상화(Abstraction)가 있습니다.


체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.

 

 

1. 캡슐화 (Encapsulation) : 하나의 객체 안에 데이터와 메서드를 묶고, 외부에서의 직접적인 접근을 제한하는 것.

사례 : 스마트폰
스마트폰은 다양한 기능과 데이터를 포함하고 있습니다. 사용자는 터치스크린이라는 인터페이스를 통해 전화, 메시지 송수신 등 다양한 기능을 사용할 수 있습니다. 하지만 스마트폰 내부의 회로 및 데이터 처리 방식은 숨겨져 있으며, 사용자가 이를 알 필요는 없습니다. 사용자는 정해진 인터페이스를 통해서만 스마트폰과 상호작용합니다.

 

2. 상속 (Inheritance) : 새로운 객체가 기존 객체의 속성과 메서드를 물려받고, 새로운 기능을 추가하는 것.

사례 : 패밀리 자동차 시리즈
자동차 제조 회사에서 '패밀리' 자동차 시리즈를 생산한다고 합시다. '패밀리 기본 모델'이라는 기본적인 프레임을 가지고 있으며, '패밀리 스포츠', '패밀리 럭셔리' 등 다양한 하위 모델이 이 기본 모델로부터 엔진, 차체 등 공통적인 특성을 물려받을 수 있습니다. 각 하위 모델은 고유의 추가 기능과 디자인을 통해 차별화됩니다.

 

3. 다형성 (Polymorphism) : 동일한 인터페이스나 메서드가 다양한 방식으로 사용될 수 있는 것.

사례 : 만능 리모컨
만능 리모컨은 TV, 에어컨 등 다양한 가전제품을 제어할 수 있습니다. '전원 버튼'이라는 동일한 버튼을 눌렀을 때 TV는 켜지거나 꺼지고, 에어컨은 작동을 멈추거나 시작할 수 있습니다. 리모컨의 같은 신호가 각기 다른 기기에서 다양한 기능으로 사용되는 것이 다형성의 예입니다.

 

4. 추상화 (Abstraction) : 복잡한 시스템에서 필요한 부분만 노출하여 중요한 기능만 하이라이트하는 것.

사례 : 자동판매기
자동판매기에서는 사용자에게 필요한 것은 음료 선택 버튼과 동전 투입구입니다. 사용자는 해당 버튼을 눌러 음료를 선택하고 돈을 넣으면 음료가 나옵니다. 내부의 복잡한 로직과 기계 장치는 사용자에게 감춰져 있으며, 사용자 관점에서 필요한 부분만 드러나 있습니다. 이처럼 꼭 필요한 부분만 추려내어 제공하는 것이 추상화입니다.

 

 


< 유연한, 변경에 용이한 개발: 다형성(Polymorphism) >

유연하고 변경에 용이하다는 것은 레고 블럭 조립하듯 컴포넌트들을 쉽고 유연하게 변경하면서 개발할 수 있다는 것을 말합니다.

객체 지향 프로그래밍의 다형성(Polymorphism)을 활용하여 유연하고 변경에 용이한 개발을 가능하게 합니다.

 

다형성을 실세계로 비유하자면 이 세상을 역할과, 그 역할을 행하는 구현으로 구분하는 것을 말합니다.

 

Ex 1) 운전자와 자동차

: 운전자는 K3를 운전하다가 아반떼로 차를 바꾸더라도 문제없이 운전할 수 있습니다. 이는 차량의 역할이 변하지 않고 단지 구현만 달라졌기 때문입니다. 운전자는 자동차의 역할에 의존하기 때문에, 자동차가 바뀌어도 운전자(클라이언트)에게 직접적인 영향은 없습니다. 이는 시스템이 유연하고 변경에 용이하다는 뜻입니다. 따라서, 운전자(클라이언트)에게 영향을 주지 않으면서도 새로운 구현을 지속적으로 제공할 수 있습니다.

 

 

Ex 2) 공연 무대

: 로미오와 줄리엣은 각각의 역할을 가진 극 중 인물이며, 다양한 배우가 이 역할을 수행할 수 있습니다. 만약 로미오가 클라이언트이고 줄리엣이 서버라고 가정한다면, 줄리엣 역할의 구현이 바뀌더라도 로미오 역할에는 영향을 미치지 않습니다. 

 

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

클라이언트는 대상의 역할(인터페이스)만 알면 충분합니다. 예를 들어, 로미오 역할은 대본에 나오는 줄리엣 역할만 알면 되며, 그 역할을 김태희가 할지 송혜교가 할지 같은 구현체에 대해서는 몰라도 됩니다.

클라이언트는 구현 대상의 내부 구조를 자세히 알 필요가 없으며, 그 구조가 변경되더라도 클라이언트에 영향을 미치지 않습니다. 또한, 클라이언트는 구현 대상을 변경하더라도(ex. K3에서 테슬라로) 영향을 받지 않습니다.

=> 이 원칙은 유연성과 변경 용이성을 높여 시스템을 더 효율적이고 유지보수가 쉽게 만듭니다.

 


< 자바에서의 다형성 >

자바(JAVA)언어에서는 객체 설계시 역할(인터페이스)를 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듦으로써 역할과 구현을 분리합니다.

  • 역할 : 인터페이스 
  • 구현 : 인터페이스를 구현한 클래스, 구현 객체

 

Ex)

- 클라이언트 역할을 하는 "MemberService"는 "MemberRepository" 인터페이스에 의존합니다.

- "MemberRepository" 인터페이스는 다양한 구현체("MemoryMemberRepository"와 "JdbcMemberRepository")를 가질 수 있습니다.

- 클라이언트인 "MemberService"는 "MemberRepository"의 인터페이스만 알면 되기 때문에 구현체가 어떤 것인지에 따라 직접적인 영향을 받지 않습니다. 이를 통해 시스템은 유연하고 변경에 용이한 구조를 가질 수 있습니다.

 

=> 다형성의 본질 : 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다.

 

 

[ 다형성의 한계 ]
역할(인터페이스)가 변하면 서버와 클라이언트 모두 큰 변경이 발생합니다.
- 자동차를 비행기로 변경한다면?
- 대본 자체가 변경된다면?

=> 그러니 인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.

 

 


< 스프링과 객체 지향 >

: 스프링은 다형성을 극대화해서 이용할 수 있게 도와줍니다.

스프링에서 이야기하는 제어의 역전(IoC)와 의존관계 주입(DI)은 다형성을 활용하여 역할과 구현을 손쉽게 다룰 수 있게 지원합니다.

스프링을 사용하면 마치 레고 블럭을 조립하거나 공연 무대의 배우를 선택하듯이, 구현체를 쉽게 변경할 수 있습니다. 이를 통해 개발자는 시스템을 더욱 유연하고 효율적으로 구축할 수 있으며, 다양한 구현체를 필요에 맞게 조합할 수 있습니다.

 

 


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

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙입니다.

1. SRP: 단일 책임 원칙(Single Responsibility Principle)
2.
OCP: 개방-폐쇄 원칙(Open/Closed Principle)
3. 
LSP: 리스코프 치환 원칙(Liskov Substitytion Principle)
4. 
ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
5. 
DIP: 의존관계 역전 원칙(Dependency Inversion Principle)

 

 


1. 단일 책임 원칙(Single responsibility principle) : 한 클래스는 하나의 책임만 가져야 한다.

: "하나의 책임"이란 개념은 다소 모호할 수 있습니다. 책임의 범위는 상황과 문맥에 따라 크거나 작을 수 있습니다.

책임을 지나치게 작게 쪼개면 기능이 너무 잘게 나뉘게 되고, 반대로 너무 크게 묶으면 책임이 지나치게 많아질 수 있습니다. 따라서, 적절한 책임의 크기를 유지하는 것이 중요합니다.

단일 책임 원칙을 잘 따르고 있는지를 판단하는 중요한 기준은 변경입니다. 변경이 있을 때 그로 인한 파급 효과가 적은 경우, 이는 단일 책임 원칙을 잘 준수한 것이라 할 수 있습니다.

 

예를 들어, UI 변경과 객체의 생성 및 사용을 분리하는 것도 단일 책임 원칙의 좋은 예입니다.

 


2. 개방-폐쇄 원칙(Open/closed principle) : 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.

: 즉, 기존에 작성되어 있던 기능을 변경하지 않고도 시스템을 확장할 수 있어야 합니다.

그러나, 일반적으로 코드 작성시 개방-폐쇄 원칙을 지키기 어렵다는 한계가 있습니다.

public class MemberService {
  private MemberRepository memberRepository = MemoryMemberRepository();
}
public class MemberService {
  // private MemberRepository memberRepository = MemoryMemberRepository();
  private MemberRepository memberRepository = JdbcMemberRepository();
}

: MemberService클라이언트가 구현 클래스를 직접 선택하고 있습니다. > 다형성을 사용했지만, 구현 객체를 변경하려면 클라이언트(MemberService)의 코드를 변경해야 합니다.(변경에 닫혀있지 않음, OCP 원칙을 지킬 수 없음)

=> 그래서 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요하여 스프링 컨테이너가 탄생하였습니다.

 


3. 리스코프 치환 원칙(Liskov Substitytion Principle) : 서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.

: 자식 클래스는 부모 클래스에서 기대되는 행동을 유지해야 한다는 뜻입니다. 다시 말해, 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것을 의미합니다. 인터페이스를 구현한 구현체를 믿고 사용하려면 이 원칙이 필요합니다.

 

예를 들어, 자동차 인터페이스의 엑셀 기능은 차를 앞으로 움직이게 해야 합니다. 그러나 구현체가 이를 뒤로 움직이도록 만들었다면, 이는 리스코프 치환 원칙(LSP)을 위반한 것입니다. 엑셀은 속도가 느리더라도 반드시 앞으로 가도록 해야 합니다.

 


4. 인터페이스 분리 원칙(Interface Segregation Principle) : 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 

: 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스가 낫습니다. 이렇게 설계하면 인터페이스가 명확해지고, 대체 가능성이 높아집니다.

 

예를 들어, 자동차 인터페이스는 운전/정비 인터페이스로 분리하고, 사용자 클라이언트는 운전자/정비사 클라이언트로 분리하는 것이 낫습니다.


5. DIP: 의존관계 역전 원칙(Dependency Inversion Principle) : 구체적인 구현이 아닌 추상화에 의존해야 한다.

: 클라이언트 코드가 구현 클래스에 의존하지 않고, 인터페이스에 의존하라는 뜻입니다. => 구현이 아닌 역할에 의존해야 합니다.

 

public class MemberService {
  private MemberRepository memberRepository = MemoryMemberRepository();
}
public class MemberService {
  // private MemberRepository memberRepository = MemoryMemberRepository();
  private MemberRepository memberRepository = JdbcMemberRepository();
}

: 앞서 나온 위 코드도 DIP를 위반한 코드입니다. MemberService는 MemberRepository인터페이스에 의존하지만, 직접 구현 클래스를 선택하고 있으므로 사실 구현클래스도 동시에 의존하고 있습니다.

 

 


< 객체 지향 설계와 스프링 >

객체 지향의 핵심은 다형성이지만, 다형성 만으로는 OCP, DIP를 지킬 수 없습니다.

이를 해결하기 위해, 스프링은 다음과 같은 기술을 도입하여 객체 지향 설계를 가능케 합니다.
- DI(Dependency Injection) : 의존관계 주입, 의존성 주입
- DI Container : 자바 객체들을 컨테이너 안에 넣어두고, 그 안에서 의존관계를 서로 연결한 후 주입 (BeanFactory, ApplicationContext)

 

: 옛날 개발자들은 좋은 객체 지향 설계를 위해 OCP(개방-폐쇄 원칙)와 DIP(의존 역전 원칙)를 지키며 개발을 시도했지만, 그 과정이 매우 복잡하고 많은 작업을 요구했습니다. 이런 문제를 해결하기 위해 결국 프레임워크를 개발하게 되었고, 그 과정에서 스프링 프레임워크, 특히 DI(의존성 주입) 컨테이너가 탄생하게 되었습니다.

=> 순수하게 자바로 OCP, DIP 원칙들을 지키면서 개발을 해보면, 결국 스프링 프레임워크를 만들게 됩니다.

 

 

 

 

 

 

 

 

 

 

 


해당 글에 포함된 코드나 그림은 김영한님이 제공해주신 자료를 바탕으로 작성되었습니다.

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

Reference : 객체 지향 설계와 스프링

 

[ 김영한 스프링 핵심 원리 - 기본편 #1 ] 객체 지향 설계와 스프링

김영한 스프링 핵심 원리 - 기본편 첫 번째 section은 객체 지향 설계와 스프링에 대한 내용이다.2000년대 초반 자바 진영에서는 EJB(Enterprise JavaBeans)를 표준으로 강력히 밀었고 실제로 많이 사용되

velog.io