본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2024 Spring Boot 스터디] 정성원 #1 주차 - 객체 지향 설계와 Spring

반응형

0. 들어가며

  Spring은 Java 개발자라면 한번쯤은 들어본 단어다. 그래서 Spring은 무엇인가?

관련 공부를 하지 않았다면 대략 “Java로 만들어진 Web Backend Framework” 라고만 알고 있을 것이다. 이번 스터디 블로깅을 통해 Spring의 기본이자 핵심이 되는 여러 원리들을 알아보고자 한다.

 

📢 본 포스트는 인프런에 업로드된 김영한님의 스프링 핵심 원리 - 기본편 강의의 내용을 일부 포함하고 있습니다.
관련 분야의 자세한 정보는 해당 강의와 우아한형제들 최연소 기술이사 출신 김영한의 스프링 완전 정복 로드맵을 참고하시길 바랍니다.

 


1. Spring과 Spring 생태계

Spring 필수 요소

  • Spring Framework (a.k.a. Spring)
    • 핵심 기술: IoC 컨테이너, AOP, 이벤트, 기타 등등…
    • 웹 기술: Spring MVC, 스프링 WebFlux
    • 데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원
    • 기술 통합: 캐시, 이메일, 원격접근, 스케줄링
    • 테스트: Spring 기반 테스트(JUnit5) 지원
    • 언어: Java, Kotlin, Groovy

  Spring Framework는 그 이름에서부터 알 수 있듯이 Spring의 근간을 이루는 주요 프레임워크다. Spring의 핵심 기술인 Bean을 이용한 의존성 주입(DI, Dependency Injection) 방식으로 제어의 역전(IoC, Inversion of Control)을 구현한 IoC 컨테이너, 특정 로직을 모듈화해 재사용성을 높히고 프로젝트 구조를 단순화할 수 있는 AOP(Aspect Oriented Programming), 서비스 간 강한 의존성을 줄이는 이벤트 등 수많은 기능들이 존재한다. 다만 Spring의 핵심이 되는 기본적인 내용만 다루기에도 그 양이 방대하니, 학습 중 모르는 부분이나 궁금한 부분이 생기면 직접 알아보도록 하자.

 

  • Spring boot
    • Spring Framework를 보다 편리하게 사용할 수 있도록 만든 프레임워크.

  Spring만으로도 많은 기능을 사용할 수 있지만, 개발자가 직접 설정 파일을 작성해야 하는 수고가 많이 든다. Bean 객체 등록, 컨테이너 구성, 객체 간 의존성 설정 및 관련 라이브러리 등록 등 해야 하는 부가적인 일이 무수히 많다. Spring Boot는 이를 해결하는 것이 주 목적인 프레임워크로, 개발자 대신 기본적인 프로젝트의 설정과 라이브러리 의존성 등 각종 설정을 자동으로 처리해준다.

 

💡 Spring은 프레임워크다.
     Spring Boot는 Spring을 빠르고 편리하게 사용할 수 있도록 도와주는 도구다.

 

 

  그렇다면 Spring을 사용해야 할 이유가 무엇일까?

Spring은 객체지향의 대표적인 언어인 Java를 기반으로 만들어진 프레임워크다. 그리고 Spring은 객체지향 언어의 가장 큰 특징들을 잘 살려주는 프레임워크다. 어떤 특징을 어떻게 살리는지는 곧 알아보도록 하자. 중요한 것은 Spring은 좋은 객체 지향 애플리케이션을 개발할 수 있도록 도와주는 프레임워크라는 것이다.


2. 객체 지향 프로그래밍

 

“좋은 객체 지향 애플리케이션의 개발”

  글귀만 봐도 좋은 말이다. 그런데, “좋은 객체 지향”은 무엇인가? 이를 알기 위해선 우선 “객체 지향 프로그래밍”이 무엇인지 알아야 한다.

 

객체 지향 프로그래밍(영어: Object-Oriented Programming, OOP)은 컴퓨터 프로그래밍패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 쉽게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다. 또한 프로그래밍을 더 배우기 쉽게 하고 소프트웨어 개발과 보수를 간편하게 하며, 보다 직관적인 코드 분석을 가능하게 하는 장점이 있다. 프로그램의 객체화 경향은 실제 세계의 모습을 그대로 반영하지 못한다는 비판을 받기도 한다.

위키백과 - 객체 지향 프로그래밍

 

  그렇다고 한다. 어려운 문장은 아니지만 간략하게 요약하자면, “여러 ‘객체’들 간의 상호작용을 통해 프로그램이 굴러가게 하는 것”이다. 여기에 위 내용 중 “유연하고 쉬운 변경”의 예시를 들면 마치 레고 블럭을 조립한다든가, USB를 꽂았다가 뺀다든가, 자석을 붙였다가 떼는 경우들이 있겠다.

 

 

 

“…그래서 객체 지향 프로그래밍의 가장 큰 특징이 뭔데?”

 

주요 객체 지향 프로그래밍의 특징은 4가지다.

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

하나씩 간단하게만 짚어보자. 각각을 깊게 알아보기엔 분량이 적지 않기에 자세한 것은 따로 찾아보길 권한다.

 

 

추상화

  • 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것.

  간략히 말해 자잘한 세부 사항은 제거하고 본질적이고 공통된 부분을 한데 모으는 것이다. 예시로는 지하철 노선도가 있겠다. 서울시 전체 지도를 펴 놓고 구불구불한 곡선을 그리는 대신, 역명과 노선 등 간략하지만 핵심만 담은 지하철 노선도가 확실히 깔끔하지 않은가?

 

캡슐화

  • 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것.

  추상화와 결이 비슷한 내용이지만 살짝 다르다. 접근 제어자Getter-Setter를 통해 클래스 내부에 있는 중요한 정보들이 외부로부터 함부로 조회되거나 변경되는 것을 막는 것을 말한다. 예시로는 이름에서부터 알 수 있듯 약(캡슐)이 있다. 약 성분이 외부 환경에 의해 변질되는 것을 막고, 이를 적절한 위치에서 작용할 수 있도록 캡슐로써 보호하는 셈이다.

 

상속

  • 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 것.

  현실 세상에서의 상속과 결이 비슷하다. 부모 클래스를 상속받은 객체는 부모 클래스의 요소를 직접 구현하지 않아도 사용이 가능하다(상황마다 약간의 차이는 있다). 자녀가 부모님의 카드를 가져다 여기저기 긁고 다니는 꿈같은 상황을 상상해보면 되겠다.

 

다형성

  • 같은 이름의 메소드나 연산자가 다른 클래스에 대해 다른 동작을 하도록 하는 것

  객체 지향 프로그래밍의 꽃, 다형성이다. 메소드 오버라이딩과 오버로딩에 대한 설명은 생략하도록 하자. 같은 부모 클래스를 상속받은 자식 클래스에서 동일한 메소드 시그니처를 사용하면, 부모 클래스를 참조하는 다른 클래스를 구현할 때 코드의 유연성과 가독성이 향상된다. 다만 디버깅 시 생각해야 할 부분이 생겨 피곤해질 수 있겠다. 관련 예시는 이곳에 담기엔 내용이 길고, 인터넷에 무수히 많으니 따로 찾아보도록 하자. 찾아보는 김에 결합도(Coupling)응집도(Cohesion)에 대해서도 알아보길 바란다.

 


3. 좋은 객체 지향 설계

  객체 지향 프로그래밍의 주요 특징을 알아봤으니, 좋은 객체 지향 설계가 무엇인지 알아볼 차례다.

바로 한 번쯤은 들어봤을, SOLID 원칙이다. 객체지향의 바이블처럼 여겨지는 “Clean Code”의 저자인 로버트 마틴이 정리한 5가지 원칙에 대해 알아보도록 하자.

 

SOLID

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

이름만 봐도 어지럽다. 하나씩 알아보자.

 

 

SRP: 단일 책임 원칙 (Single Responsibility Principle)

  • 하나의 클래스는 하나의 책임만 가져야 한다.

  “책임”이 모호해 보일 수 있다. 대신 “하는 일”, 혹은 “역할”이라고 바꾸면 이해가 빨라진다. 예를 들자면 DB에 저장하는 역할을 맡은 클래스에서 쿠폰 할인율을 계산하는 일까지 맡기지 말라는 뜻이다.

 

OCP: 개방-폐쇄 원칙 (Open/Closed Principle)

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

  무슨 말인가 싶다. 확장이 곧 변경 아닌가? 다형성을 활용하라는 뜻이다. 인터페이스를 만들고 이를 참조하게 하면, 스펙 변경으로 인해 새로운 구현체를 만들어야 할 때에도 기존 코드를 변경하지 않아도 된다.

(사실 한 두 줄 정도는 변경해야 하기도 한다. 이마저도 없애기 위한 방법이 있으나 심화 내용이므로 따로 알아보도록 하자.)

 

LSP: 리스코프 치환 원칙 (Liskov Substitution Principle)

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

  진짜 무슨 소리인지 감도 오지 않는다. 이름을 왜 이렇게 지었는지는 모르겠지만, 쉽게 표현하자면 “하기로 한 일을 하자”는 것이다. 예시를 붙이자면 자동차 인터페이스에서 엑셀을 밟으면 앞으로 가기로 만들어 놓고, 구현체에서 후진하도록 만들지 말라는 것이다.

 

ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)

  • 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

  맞는 말인 듯 하지만 무슨 소리인지 모르겠다. 다르게 표현하자면, “범용 인터페이스 하나보다 특정 인터페이스 여럿이 낫다”는 것이다. 스마트폰 추상 클래스 안에 통화 인터페이스와 지문 인식 인터페이스가 있다고 하자. 신형 스마트폰을 만들 때는 두 인터페이스가 모두 쓰이기에 문제가 없다. 하지만 지문 인식 기능이 없는 구형 디바이스에 프로그램을 넣어야 하는 상황이 나타났을 때 두 인터페이스가 하나로 합쳐져 있었다면? 사용하지도 못하는 지문 인식 인터페이스까지 구현해야 했을 것이다.

 

DIP: 의존관계 역전 원칙 (Dependency Inversion Principle)

  • 객체는 구체적인 객체가 아닌 추상화에 의존해야 한다.

  변하기 쉬운 구현체에 의존했다가 상황이 변해 다른 구현체로 교체해야 하는 일이 생기면 기존 코드를 수정하는 일이 생긴다. 아이 클래스가 로봇 장난감 구현체를 의존하다가, 다른 장난감으로 교체하면 아이 클래스까지도 수정해야 한다. 구현체 대신 인터페이스(역할)에 의존하도록 하자.

 


 

4. SoC, DI, IoC, Container

유료 강의의 코드를 모조리 긁어 올릴 수는 없기에, 꼭 필요한 부분만 적당히 짚어가며 핵심적인 개념들을 알아보자.

  위 다이어그램을 보자.

주문 서비스 구현체(OrderServiceImpl)가 할인 정책 인터페이스(DiscountPolicy)에 의존하고 있으며, 정액 할인(FixDiscountPolicy)과 정률 할인 정책(RateDiscountPolicy)이 위 인터페이스에 의존한다. 코드는 아래 이어진다.

 

그림 상 하나로 합쳐져 있지만, RateDiscountPolicy가 없었다가 새로 생긴 상황을 가정한다.

OrderService 인터페이스의 createOrder를 구현한 OrderServiceImpl 클래스.

 

DiscountPolicy 인터페이스의 discount를 정액 할인 정책으로 구현한 FixDiscountPolicy 클래스.

 

이때 할인 정책을 새로운 정률 할인 방식으로 변경하기로 해 새로운 정책 RateDiscountPolicy를 만들었다.

새로운 정률 할인 방식을 구현한 RateDiscountPolicy.

 

 

기존 할인 정책을 새 할인 정책으로 변경해보자.

 

어?

 

어? 새로운 구현 내용이 기존 코드에 영향을 미치고 있다. ⇒ OCP 위반.

그리고, 인터페이스 뿐만 아니라 구현체에도 의존하고 있다. ⇒ DIP 위반.

 

 

처음 다이어그램의 의존관계 형식을 기대했던 다르게도, 실제 구현된 의존관계는 아래처럼 구현체에도 의존하고 있다.

 

 

인터페이스 뿐만 아니라 구현체에도 의존하고 있었다!

 

이를 해결하려면, 위 사진의 "new RateDiscountPolicy()" 부분을 지워야 한다. 그렇다고 지우고 실행하자니, 콘솔에서 NullPointerException이 반갑게 기다리고 있을 것이 뻔하다. 그러면 어떻게 해결할 수 있을까?

 

답은 “다른 누군가가 대신 구현체를 넣어주기”다.

 

관심사의 분리: SoC (Seperation of Concerns)

 

  공연 이야기를 해 보자. 공연을 구성할 때, 각 배역에 누가 들어가 연기할지는 배우가 정하는 것이 아니다. 공연 기획자(감독)이 정하는 것이다. 무슨 소리인가 싶겠지만, “관심사의 분리” 키워드와 엮어 생각해보자. 배우의 관심사는 받은 배역을 연기하는 것이고, 공연 감독의 관심사는 누구에게 어떤 배역을 맡길지 결정하는 것이다. 연기는 배우가, 캐스팅은 감독이 하듯이 각자 해야 할 일에 집중하도록 해야 좋은 공연이 만들어진다.

 

  다시 프로그램으로 돌아와서, 앞에서 OCP와 DIP가 지켜지지 않은 이유를 생각해보자. 그 이유는 공연 상황에 빗대어 말하자면 “배우가 감독 역할까지 관심을 가졌기 때문”이다.

“주문 서비스(OrderServiceImpl)에서 정액 할인 정책(FixDiscountPolicy)을 선택했기 때문” 에 벌어진 일이라는 말이다.

 

그렇다면 공연 상황처럼, 적절한 구현체를 선택하는 일을 맡을 클래스가 별도로 존재하면 해결될 일 아니겠는가?

그래서 그런 일을 맡아줄 클래스를 만들었다.

 

AppConfig 클래스를 통해 orderService()를 호출하면, 생성자를 통해 필요한 구현체를 넣어 인스턴스를 생성해 동작하게 할 것이다.

 

AppConfig를 추가한 이후의 다이어그램

 

 

 

이런 다이어그램의 흐름을 가지는 것이다.

 

이번에야말로 OrderServiceImpl이 인터페이스에만 의존하므로 DIP가 해결되었고, 프로그램 사용 영역 외인 구성 영역에서 구현체를 생성해주니 사용 영역의 코드는 변경하지 않아도 프로그램 확장이 가능해졌으니 OCP도 해결되었다!

 

 

@See Also: 책임 할당을 위한 GRASP 패턴 - 책임 중심 설계를 위해 책임 할당하기 위한 이야기

가볍게 쓱 읽어보자. 이런 것도 있다. 3학년 객체지향분석및설계 과목에서 배울 수 있으니 참고.

 

제어의 역전: IoC(Inversion of Control), 그리고
의존관계 주입: DI(Dependency Injection)

 

  기존에는 구현체(OrderServiceImpl)에서 스스로 필요한 구현체(xxxDiscountPolicy)를 선택하고, 제어 흐름을 직접 조종했다. 하지만 AppConfig의 등장으로 구현체는 자신의 로직의 실행에만 집중할 수 있게 되었다. AppConfig가 제어의 흐름을 가져간 것이다. 이처럼 프로그램의 제어 흐름을 프로그램 내에서 관리하지 않고 외부에서 관리하는 것을 제어의 역전(IoC: Inversion of Control)이라고 한다.

 

추가로, 실행 시점(런타임)의 객체 간 관계 다이어그램을 살펴보자.

런타임에서의 흐름이므로 각 객체의 인스턴스가 생성되는 시점에 위 그림에서의 흐름대로 의존관계가 주입되면서 흐름상 하위 단계의 구현체가 만들어진다. 이처럼 “실행 시점(런타임)에서 클라이언트와 서버의 의존관계가 연결되는 것”을 의존관계 주입(DI: Dependency Injection)이라고 한다.

 

Container (IoC Container, DI Container)

위에 있는 AppConfig처럼 객체를 생성하고 관리하며 의존관계를 관리하는 클래스를 IoC Container 혹은 DI Container라고 한다. 요즘은 DI에 더 초점을 맞추어 주로 DI 컨테이너라고 한다고 한다. 이외에도 Assembler, Object Factory 등으로 부르기도 한단다.

 


 

 

Reference

https://www.inflearn.com/course/스프링-핵심-원리-기본편

https://ko.wikipedia.org/wiki/객체_지향_프로그래밍

https://www.codestates.com/blog/content/객체-지향-프로그래밍-특징

https://velog.io/@hwisaac/OOP-다형성

https://blog.itcode.dev/posts/2021/08/11/inheritance

https://www.inflearn.com/blogs/3315

https://blog.itcode.dev/posts/2021/08/16/interface-segregation-principle

https://velog.io/@harinnnnn/OOP-객체지향-5대-원칙SOLID-의존성-역전-원칙-DIP

https://ko.wikipedia.org/wiki/관심사_분리

 

 
반응형